commit 098f59b644e7d8399623ec89eeb7df466d88a88b Author: rnsrk Date: Sun Apr 6 22:48:06 2025 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dc9882a --- /dev/null +++ b/.gitignore @@ -0,0 +1,93 @@ +# General +.cursor +.env +**/.env +secrets/* +!secrets/.gitkeep +**/.git +**/.github + +# Drupal +drupal/drupal/root +drupal/nginx/nginx.conf + +# Mailcow +mailcow/.env +mailcow/.env.backup +!mailcow/data/conf/nginx/dynmaps.conf +!mailcow/data/conf/nginx/meta_exporter.conf +!mailcow/data/conf/nginx/site.conf +!mailcow/data/**/.gitkeep +mailcow/data/*.iml +mailcow/data/.idea +mailcow/data/.vscode/* +mailcow/data/assets/ssl-example/* +mailcow/data/assets/ssl/* +mailcow/data/conf/borgmatic/ +mailcow/data/conf/clamav/whitelist.ign2 +mailcow/data/conf/dovecot/acl_anyone +mailcow/data/conf/dovecot/dovecot-master.passwd +mailcow/data/conf/dovecot/dovecot-master.userdb +mailcow/data/conf/dovecot/extra.conf +mailcow/data/conf/dovecot/mail_replica.conf +mailcow/data/conf/dovecot/global_sieve_* +mailcow/data/conf/dovecot/last_login +mailcow/data/conf/dovecot/lua +mailcow/data/conf/dovecot/mail_plugins* +mailcow/data/conf/dovecot/shared_namespace.conf +mailcow/data/conf/dovecot/sni.conf +mailcow/data/conf/dovecot/sogo-sso.conf +mailcow/data/conf/dovecot/sogo_trusted_ip.conf +mailcow/data/conf/dovecot/sql +mailcow/data/conf/dovecot/conf.d/fts.conf +mailcow/data/conf/nextcloud-*.bak +mailcow/data/conf/nginx/*.active +mailcow/data/conf/nginx/*.bak +mailcow/data/conf/nginx/*.conf +mailcow/data/conf/nginx/*.custom +mailcow/data/conf/phpfpm/sogo-sso/sogo-sso.pass +mailcow/data/conf/portainer/ +mailcow/data/conf/postfix/allow_mailcow_local.regexp +mailcow/data/conf/postfix/custom_postscreen_whitelist.cidr +mailcow/data/conf/postfix/custom_transport.pcre +mailcow/data/conf/postfix/extra.cf +mailcow/data/conf/postfix/sni.map +mailcow/data/conf/postfix/sni.map.db +mailcow/data/conf/postfix/sql +mailcow/data/conf/postfix/dns_blocklists.cf +mailcow/data/conf/postfix/dnsbl_reply.map +mailcow/data/conf/rspamd/custom/* +mailcow/data/conf/rspamd/local.d/* +mailcow/data/conf/rspamd/override.d/* +mailcow/data/conf/sogo/custom-theme.js +mailcow/data/conf/sogo/plist_ldap +mailcow/data/conf/sogo/plist_ldap.sh +mailcow/data/conf/sogo/sieve.creds +mailcow/data/conf/sogo/cron.creds +mailcow/data/conf/sogo/custom-fulllogo.svg +mailcow/data/conf/sogo/custom-shortlogo.svg +mailcow/data/conf/sogo/custom-fulllogo.png +mailcow/data/gitea/ +mailcow/data/gogs/ +mailcow/data/hooks/dovecot/* +mailcow/data/hooks/phpfpm/* +mailcow/data/hooks/postfix/* +mailcow/data/hooks/rspamd/* +mailcow/data/hooks/sogo/* +mailcow/data/hooks/unbound/* +mailcow/data/web/templates/cache/* +!mailcow/data/web/templates/cache/.gitkeep +mailcow/data/web/.well-known/acme-challenge +mailcow/data/web/css/build/0081-custom-mailcow.css +mailcow/data/web/inc/vars.local.inc.php +mailcow/data/web/inc/app_info.inc.php +mailcow/data/web/nextcloud*/ +mailcow/data/web/rc*/ +mailcow/docker-compose.override.yml +mailcow/mailcow.conf +mailcow/mailcow.conf_backup +mailcow/rebuild-images.sh +mailcow/refresh_images.sh +mailcow/update_diffs/ +mailcow/create_cold_standby.sh +!mailcow/data/conf/nginx/mailcow_auth.conf diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 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 General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is 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. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + 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. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + 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 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. Use with the GNU Affero General Public License. + + 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 Affero 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 special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 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 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 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 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + 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 GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4df6f68 --- /dev/null +++ b/README.md @@ -0,0 +1,191 @@ +# Open Productive Stack +## Overview +This repository contains a productive stack of open-source applications for team collaboration and communication. The stack includes: + +### Core Infrastructure +- **Traefik**: Edge router that handles routing and load balancing for all services +- **PostgreSQL**: Relational database for applications requiring PostgreSQL +- **MariaDB**: MySQL-compatible database for applications requiring MySQL +- **Adminer**: Database management tool for easy database administration + +### Mail +- **Mailcow**: Complete mail server solution with SMTP, IMAP, antivirus, and webmail + +### Collaboration Tools +- **Nextcloud**: Self-hosted file sync and share platform with collaboration features +- **OnlyOffice**: Online office suite for document editing and collaboration +- **OpenProject**: Project management and team collaboration software +- **HedgeDoc**: Collaborative markdown notes editor for team documentation + +### Web Publishing +- **Drupal**: Flexible content management system (CMS) for building websites + +All components are containerized using Docker for easy deployment, scaling, and management, creating a complete productivity environment for teams. + +## Requirements +You need [docker](https://docs.docker.com/get-started/get-docker/) and with [docker compose plugin](https://docs.docker.com/compose/). You may want to follow the [post installation instructions](https://docs.docker.com/engine/install/linux-postinstall/). + +At least 6 cores with 16GB RAM 100GB SSD would be sufficent. + +## Install +### Core environment +1) Copy the `.example-env` to `.env` and add you settings. +```bash +cp core/.example-env core/.env +``` + +2) Start traefik, mariadb, postgres and adminer with: +```bash +docker compose -f core/docker-compose.yml up -d +``` + +### Drupal +1) Copy the `.example-env` to `.env` and add you settings. +```bash +cp drupal/.example-env drupal/.env +``` +2) Create databse and Drupal root. +```bash +drupal/create_infra.bash +``` + +3) Start Drupal containers. +```bash +docker compose -f drupal/docker-compose.yml up -d +``` + +#### Additional steps +You may want to use Redis Caching. +3) add to drupal/sites/default/settings.php: +```php +// Redis Configuration +$settings['redis.connection']['interface'] = 'PhpRedis'; +$settings['redis.connection']['host'] = 'redis'; +$settings['redis.connection']['port'] = 6379; +$settings['cache']['default'] = 'cache.backend.redis'; +$settings['cache']['bins']['bootstrap'] = 'cache.backend.chainedfast'; +$settings['cache']['bins']['discovery'] = 'cache.backend.chainedfast'; +$settings['cache']['bins']['config'] = 'cache.backend.chainedfast'; +``` + +4) Visit your Domain and install Drupal site. + +### Gitlab +1) Copy the `.example-env` to `.env` and add you settings. +```bash +cp gitlab/.example-env gitlab/.env +``` + +2) Start gitlab. +```bash +docker compose -f gitlab/docker-compose.yml up -d +``` + +3) Get your root password. +```bash +sudo docker exec -it gitlab grep 'Password:' +/etc/gitlab/initial_root_password +``` + +4) Visit you domain and log in. + +# Hedgedoc +1) Copy the `.example-env` to `.env` and add you settings. +```bash +cp hedgedoc/.example-env hedgedoc/.env +``` +2) Create database and Drupal root. +```bash +hedgedoc/create_infra.bash +``` + +3) Start containers. +```bash +docker compose -f hedgedoc/docker-compose.yml up -d +``` + +4) Add your user. +```bash +source hedgedoc/.env +docker exec hedgedoc bin/manage_users --pass ${HEDGEDOC_USER_PASSWORD} --add ${HEDGEDOC_USER_EMAIL} +``` + + +### Mailcow +1) Generate config +```bash +cd mailcow +./geneare_config.bash +``` +2) Copy config to .env +```bash +cp mailcow.conf .env +``` + +3) Start containers. +```bash +docker compose docker-compose.yml up -d +``` + +4) Visit DOMAIN/admin and log in with admin:admin. + +5) Consider the post installation steps, i. e. + +[watchdog](https://docs.mailcow.email/de/post_installation/firststeps-authorize_watchdog_and_bounces/) +[dmarc](https://docs.mailcow.email/de/post_installation/firststeps-dmarc_reporting/) +[dkim](https://docs.mailcow.email/getstarted/prerequisite-dns/) (You get your dkim key when you registered your email domain in mailcow ui) + +### OnylOffice +1) Copy the `.example-env` to `.env` and add you settings. +```bash +cp onlyoffice/.example-env onlyoffice/.env +``` +2) Create database and Drupal root. +```bash +onlyoffice/create_infra.bash +``` + +3) Start containers. +```bash +docker compose -f onlyoffice/docker-compose.yml up -d +``` + +### Nextcloud +1) Copy the `.example-env` to `.env` and add you settings. +```bash +cp nextcloud/.example-env nextcloud/.env +``` +2) Create database and Drupal root. +```bash +nextcloud/create_infra.bash +``` + +3) Start containers. +```bash +docker compose -f nextcloud/docker-compose.yml up -d +``` + +4) Visit nextcloud domain and login with your .env credentials. + +### Openproject +1) Copy the `.example-env` to `.env` and add you settings. +```bash +cp nextcloud/.example-env nextcloud/.env +``` +2) Create database and Drupal root. +```bash +hedgedoc/create_infra.bash +``` + +3) Start containers. +```bash +docker compose -f hedgedoc/docker-compose.yml up -d +``` + +4) Visit openproject domain and login with admin:admin and set new password. + +### Roadmap +- Tweak the core components and subservices for petter performance +- More automatisation when installinge the environment +- Add more services + - [ ] Matrix/Synapse + Element diff --git a/core/.example-env b/core/.example-env new file mode 100644 index 0000000..5334ef3 --- /dev/null +++ b/core/.example-env @@ -0,0 +1,17 @@ +# General +DOMAIN= + +# MariaDB +MARIADB_ROOT_PASSWORD= +MARIADB_USER= +MARIADB_PASSWORD= + +# Postgres +POSTGRES_PASSWORD= +POSTGRES_USER= + +# Traefik +TRAEFIK_USERNAME= +TRAEFIK_PASSWORD= +TRAEFIK_EMAIL= +TRAEFIK_HASHED_PASSWORD= diff --git a/core/docker-compose.yml b/core/docker-compose.yml new file mode 100644 index 0000000..7e7cae1 --- /dev/null +++ b/core/docker-compose.yml @@ -0,0 +1,130 @@ +services: + # Database-Stack + adminer: + image: adminer + container_name: adminer + depends_on: + - mariadb + - traefik + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik" + - "traefik.http.routers.adminer.rule=Host(`adminer.${DOMAIN}`)" + - "traefik.http.routers.adminer.entrypoints=web,websecure" + - "traefik.http.routers.adminer.middlewares=https-redirect" + - "traefik.http.routers.adminer.tls=true" + - "traefik.http.routers.adminer.tls.certresolver=le" + - "traefik.http.services.adminer.loadbalancer.server.port=8080" + networks: + - database + - traefik + restart: unless-stopped + + mariadb: + image: mariadb:11.5.2 + container_name: mariadb + environment: + MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD} + MARIADB_USER: ${MARIADB_USER} + MARIADB_PASSWORD: ${MARIADB_PASSWORD} + labels: + - "traefik.enable=false" + volumes: + - mariadb-data:/var/lib/mysql + networks: + - database + restart: unless-stopped + + postgres: + image: postgres:17 + container_name: postgres + environment: + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + volumes: + - postgres-data:/var/lib/postgresql + networks: + - database + restart: unless-stopped + + # Traefik + traefik: + image: traefik:3.3 + container_name: traefik + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik" + # Middlewares + - "traefik.http.middlewares.admin-auth.basicauth.users=${TRAEFIK_USERNAME}:${TRAEFIK_HASHED_PASSWORD}" + - "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https" + - "traefik.http.middlewares.https-redirect.redirectscheme.permanent=true" + - "traefik.http.middlewares.https-redirect.redirectscheme.port=443" + - "traefik.http.middlewares.nextcloud-headers.headers.stsSeconds=15552000" + - "traefik.http.middlewares.nextcloud-headers.headers.stsIncludeSubdomains=true" + - "traefik.http.middlewares.nextcloud-headers.headers.stsPreload=true" + - "traefik.http.middlewares.nextcloud-headers.headers.forceSTSHeader=true" + + # routers + - "traefik.http.routers.traefik.rule=Host(`traefik.${DOMAIN}`)" + - "traefik.http.routers.traefik.entrypoints=web,websecure" + - "traefik.http.routers.traefik.middlewares=admin-auth,https-redirect" + - "traefik.http.routers.traefik.tls=true" + - "traefik.http.routers.traefik.tls.certresolver=le" + - "traefik.http.routers.traefik.service=api@internal" + + # Services + - "traefik.http.services.traefik.loadbalancer.server.port=8080" + + command: + # Enable Docker provider + - --providers.docker + # Disable exposing services without Traefik labels + - --providers.docker.exposedbydefault=false + # Listen on port 2424 for SSH requests + - --entrypoints.gitlab-ssh.address=:2424 + # Listen on port 80 for HTTP requests + - --entrypoints.web.address=:80 + # Listen on port 443 for HTTPS requests + - --entrypoints.websecure.address=:443 + # Redirect HTTP requests to HTTPS + - --entrypoints.web.http.redirections.entryPoint.to=websecure + - --entrypoints.web.http.redirections.entryPoint.scheme=https + - --entrypoints.web.http.redirections.entrypoint.permanent=true + # Use the specified email address for Let's Encrypt certificate requests + - --certificatesresolvers.le.acme.email=${TRAEFIK_EMAIL} + # Use the HTTP challenge for Let's Encrypt certificate requests + - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web + # Use the specified storage location for Let's Encrypt certificates + - --certificatesresolvers.le.acme.storage=/certificates/acme.json + # Use the TLS-ALPN-01 challenge for Let's Encrypt certificate requests + - --certificatesresolvers.le.acme.tlschallenge=true + # Enable access log output + - --accesslog + # Enable general log output + - --log.level=INFO + # Enable the Traefik API + - --api + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - server-certificates:/certificates + ports: + - 2424:2424 + - 80:80 + - 443:443 + networks: + - traefik + restart: unless-stopped + +volumes: + mariadb-data: + name: mariadb-data + postgres-data: + name: postgres-data + server-certificates: + name: server-certificates + +networks: + database: + name: database + traefik: + name: traefik diff --git a/diagnostic.sh b/diagnostic.sh new file mode 100755 index 0000000..4b95ca1 --- /dev/null +++ b/diagnostic.sh @@ -0,0 +1,164 @@ +#!/bin/bash + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +echo -e "${YELLOW}Running diagnostic checks for Open Productive Stack...${NC}" + +# Function to check if a service is running +check_service() { + local service=$1 + + echo -e "${YELLOW}Checking if $service is running...${NC}" + if docker ps | grep -q "$service"; then + echo -e "${GREEN}$service is running.${NC}" + return 0 + else + echo -e "${RED}$service is not running.${NC}" + return 1 + fi +} + +# Function to check network connectivity +check_connectivity() { + local service=$1 + local port=$2 + local host=${3:-localhost} + + echo -e "${YELLOW}Checking connectivity to $service on $host:$port...${NC}" + if nc -z -v -w5 "$host" "$port" 2>/dev/null; then + echo -e "${GREEN}Connection to $service on $host:$port successful.${NC}" + return 0 + else + echo -e "${RED}Cannot connect to $service on $host:$port.${NC}" + return 1 + fi +} + +# Function to check Traefik configuration +check_traefik() { + echo -e "${YELLOW}Checking Traefik configuration...${NC}" + + # Check if Traefik is running + check_service "traefik" || return 1 + + # Check Traefik ports + check_connectivity "Traefik HTTP" 80 || echo -e "${RED}Traefik HTTP port not accessible.${NC}" + check_connectivity "Traefik HTTPS" 443 || echo -e "${RED}Traefik HTTPS port not accessible.${NC}" + check_connectivity "Traefik SSH" 2424 || echo -e "${RED}Traefik SSH port not accessible.${NC}" + + # Check Traefik certificates + echo -e "${YELLOW}Checking Traefik certificates...${NC}" + if docker exec traefik ls -la /certificates/acme.json >/dev/null 2>&1; then + echo -e "${GREEN}Traefik certificates found.${NC}" + else + echo -e "${RED}Traefik certificates not found.${NC}" + fi + + return 0 +} + +# Function to check GitLab configuration +check_gitlab() { + echo -e "${YELLOW}Checking GitLab configuration...${NC}" + + # Check if GitLab is running + check_service "gitlab" || return 1 + + # Check GitLab HTTP port + docker exec gitlab grep -q "external_url" /etc/gitlab/gitlab.rb && \ + echo -e "${GREEN}GitLab external URL is configured.${NC}" || \ + echo -e "${RED}GitLab external URL is not configured.${NC}" + + # Check GitLab SSH port + docker exec gitlab grep -q "gitlab_shell_ssh_port" /etc/gitlab/gitlab.rb && \ + echo -e "${GREEN}GitLab SSH port is configured.${NC}" || \ + echo -e "${RED}GitLab SSH port is not configured.${NC}" + + # Check GitLab SSH connection + echo -e "${YELLOW}Checking GitLab SSH connection...${NC}" + if ssh -T git@gitlab.${DOMAIN} -p 2424 -o StrictHostKeyChecking=no -o BatchMode=yes &>/dev/null; then + echo -e "${GREEN}GitLab SSH connection successful.${NC}" + else + echo -e "${RED}GitLab SSH connection failed. This is expected if you haven't set up SSH keys yet.${NC}" + echo -e "${YELLOW}Try: ssh -vT git@gitlab.${DOMAIN} -p 2424${NC}" + fi + + return 0 +} + +# Function to check database services +check_databases() { + echo -e "${YELLOW}Checking database services...${NC}" + + # Check MariaDB + check_service "mariadb" && \ + echo -e "${GREEN}MariaDB is running.${NC}" || \ + echo -e "${RED}MariaDB is not running.${NC}" + + # Check PostgreSQL + check_service "postgres" && \ + echo -e "${GREEN}PostgreSQL is running.${NC}" || \ + echo -e "${RED}PostgreSQL is not running.${NC}" + + return 0 +} + +# Function to check all other services +check_all_services() { + echo -e "${YELLOW}Checking all services...${NC}" + + local services=("traefik" "gitlab" "mariadb" "postgres" "adminer" "nextcloud" "onlyoffice" "openproject" "hedgedoc" "drupal") + + for service in "${services[@]}"; do + check_service "$service" + done + + return 0 +} + +# Check Docker and Docker Compose +echo -e "${YELLOW}Checking Docker and Docker Compose installation...${NC}" +if command -v docker >/dev/null 2>&1; then + echo -e "${GREEN}Docker is installed: $(docker --version)${NC}" +else + echo -e "${RED}Docker is not installed!${NC}" + exit 1 +fi + +if docker compose version >/dev/null 2>&1; then + echo -e "${GREEN}Docker Compose plugin is installed: $(docker compose version)${NC}" +else + echo -e "${RED}Docker Compose plugin is not installed!${NC}" + exit 1 +fi + +# Check system resources +echo -e "${YELLOW}Checking system resources...${NC}" +echo -e "${YELLOW}CPU:${NC} $(grep -c processor /proc/cpuinfo) cores" +echo -e "${YELLOW}Memory:${NC} $(free -h | grep Mem | awk '{print $2}')" +echo -e "${YELLOW}Disk space:${NC} $(df -h / | awk 'NR==2 {print $2}')" + +# Domain configuration from .env file +if [ -f "./core/.env" ]; then + source ./core/.env + echo -e "${YELLOW}Domain configuration:${NC} ${DOMAIN}" +else + echo -e "${RED}Core .env file not found!${NC}" + DOMAIN="example.com" +fi + +# Run specific checks +check_traefik +echo "" +check_gitlab +echo "" +check_databases +echo "" +check_all_services + +echo -e "${GREEN}Diagnostic checks completed.${NC}" +echo -e "${YELLOW}For detailed logs, run: docker logs ${NC}" diff --git a/drupal/.example-env b/drupal/.example-env new file mode 100644 index 0000000..b7cffe8 --- /dev/null +++ b/drupal/.example-env @@ -0,0 +1,8 @@ +DOMAIN= +DRUPAL_DB_HOST=postgres +DRUPAL_DB_NAME= +DRUPAL_DB_PASSWORD= +DRUPAL_DB_PORT= +DRUPAL_DB_USER= +DRUPAL_VERSION= + diff --git a/drupal/create_infra.bash b/drupal/create_infra.bash new file mode 100755 index 0000000..15a68cc --- /dev/null +++ b/drupal/create_infra.bash @@ -0,0 +1,14 @@ +#!/bin/bash + + +if [ ! -d "drupal/root" ]; then + mkdir -p drupal/root + docker run --rm drupal:11.1.6-php8.4-fpm-bookworm tar -cC /opt/drupal/ . | tar -xC drupal/root +fi + +source ../core/.env +source .env + +docker exec postgres psql -U $POSTGRES_USER -d postgres -c "CREATE USER $DRUPAL_DB_USER WITH PASSWORD '$DRUPAL_DB_PASSWORD';" +docker exec postgres psql -U $POSTGRES_USER -d postgres -c "CREATE DATABASE $DRUPAL_DB_NAME OWNER $DRUPAL_DB_USER;" +docker exec postgres psql -U $POSTGRES_USER -d $DRUPAL_DB_NAME -c "GRANT ALL PRIVILEGES ON DATABASE $DRUPAL_DB_NAME TO $DRUPAL_DB_USER;" diff --git a/drupal/docker-compose.yml b/drupal/docker-compose.yml new file mode 100644 index 0000000..08081e7 --- /dev/null +++ b/drupal/docker-compose.yml @@ -0,0 +1,70 @@ +services: + nginx: + image: drupal-nginx + build: + context: ./nginx + dockerfile: Dockerfile + args: + DOMAIN: ${DOMAIN} + container_name: drupal-reverse-proxy + labels: + - traefik.enable=true + - traefik.docker.network=traefik + - traefik.http.routers.drupal-reverse-proxy.rule=Host(`${DOMAIN}`) + - traefik.http.routers.drupal-reverse-proxy.entrypoints=web,websecure + - traefik.http.routers.drupal-reverse-proxy.middlewares=https-redirect + - traefik.http.routers.drupal-reverse-proxy.tls=true + - traefik.http.routers.drupal-reverse-proxy.tls.certresolver=le + - traefik.http.services.drupal-reverse-proxy.loadbalancer.server.port=80 + volumes: + - ./drupal/root/web:/var/www/html + networks: + - traefik + - drupal + + drupal-fpm: + image: drupal-php8-4-fpm-bookworm + build: + context: ./drupal + dockerfile: Dockerfile + args: + DRUPAL_VERSION: ${DRUPAL_VERSION:-11.1.6} + labels: + - traefik.enable=false + container_name: drupal-fpm + expose: + - "9000" + volumes: + - ./drupal/root:/opt/drupal + networks: + - database + - drupal + + redis: + image: redis:7-alpine + container_name: drupal-redis + command: redis-server --loglevel warning + environment: + - OVERC + volumes: + - redis-data:/data + networks: + - drupal + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + +volumes: + redis-data: + name: drupal-redis-data + +networks: + traefik: + external: true + database: + external: true + drupal: + name: drupal diff --git a/drupal/drupal/Dockerfile b/drupal/drupal/Dockerfile new file mode 100644 index 0000000..b11e33e --- /dev/null +++ b/drupal/drupal/Dockerfile @@ -0,0 +1,32 @@ +ARG DRUPAL_VERSION + +FROM drupal:${DRUPAL_VERSION}-php8.4-fpm-bookworm + +RUN apt-get update && apt-get install -y \ + git \ + vim \ + && rm -rf /var/lib/apt/lists/* + +# Upload progress +RUN set -eux; \ + git clone https://github.com/php/pecl-php-uploadprogress/ /usr/src/php/ext/uploadprogress/; \ + docker-php-ext-configure uploadprogress; \ + docker-php-ext-install uploadprogress; \ + rm -rf /usr/src/php/ext/uploadprogress; + +# Install apcu +RUN set -eux; \ + pecl install apcu; + +# Add php configs +RUN { \ + echo 'extension=apcu.so'; \ + echo "apc.enable_cli=1"; \ + echo "apc.enable=1"; \ + echo "apc.shm_size=32M"; \ + } >> /usr/local/etc/php/conf.d/zz-apcu-custom.ini; + +# Enable output buffering +RUN { \ + echo 'output_buffering = on'; \ + } >> /usr/local/etc/php/conf.d/zz-drupal-recommended.ini; diff --git a/drupal/nginx/Dockerfile b/drupal/nginx/Dockerfile new file mode 100644 index 0000000..0ffadfd --- /dev/null +++ b/drupal/nginx/Dockerfile @@ -0,0 +1,8 @@ +FROM nginx:latest + +COPY ./nginx.conf.template /etc/nginx/nginx.conf.template + +ARG DOMAIN +RUN sed 's|${DOMAIN}|'"$DOMAIN"'|g' /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf + +ENTRYPOINT ["nginx", "-g", "daemon off;"] diff --git a/drupal/nginx/nginx.conf.template b/drupal/nginx/nginx.conf.template new file mode 100644 index 0000000..5aee951 --- /dev/null +++ b/drupal/nginx/nginx.conf.template @@ -0,0 +1,66 @@ +user www-data; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + access_log /var/log/nginx/access.log main; + sendfile on; + keepalive_timeout 65; + gzip on; + + server { + listen 80; + server_name ${DOMAIN}; + root /var/www/html; + + location / { + try_files $uri /index.php$is_args$args; + } + + location ~ \.php$ { + fastcgi_pass drupal-fpm:9000; + fastcgi_split_path_info ^(.+\.php)(/.*)$; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param DOCUMENT_ROOT $document_root; + } + + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { + try_files $uri @rewrite; + expires max; + log_not_found off; + } + + location @rewrite { + rewrite ^ /index.php; + } + + # Don't allow direct access to PHP files in the vendor directory + location ~ /vendor/.*\.php$ { + deny all; + return 404; + } + + # Protect files and directories from prying eyes + location ~* \.(engine|inc|install|make|module|profile|po|sh|.*sql|theme|twig|tpl(\.php)?|xtmpl|yml)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^(\.(?!well-known).*|Entries.*|Repository|Root|Tag|Template|composer\.(json|lock)|web\.config)$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig|\.save)$ { + deny all; + return 404; + } + + # Protect .git directory + location ~ /\.git { + deny all; + return 404; + } + } +} diff --git a/gitlab/docker-compose.yml b/gitlab/docker-compose.yml new file mode 100644 index 0000000..eab4cae --- /dev/null +++ b/gitlab/docker-compose.yml @@ -0,0 +1,54 @@ +services: + gitlab: + image: gitlab/gitlab-ce:17.8.6-ce.0 + container_name: gitlab + hostname: '${GITLAB_DOMAIN}' + environment: + GITLAB_OMNIBUS_CONFIG: | + # Add any other gitlab.rb configuration here, each on its own line + external_url 'https://${GITLAB_DOMAIN}' + gitlab_rails['gitlab_shell_ssh_port'] = 2424 + # We need to tell GitLab to use SSH port 22 internally + gitlab_shell['auth_file'] = "/var/opt/gitlab/.ssh/authorized_keys" + #gitlab_shell['ssh_port'] = 22 + nginx['listen_port'] = 80 + nginx['listen_https'] = false + nginx['proxy_set_headers'] = { + "X-Forwarded-Proto" => "https", + "X-Forwarded-Ssl" => "on" + } + labels: + - traefik.enable=true + - traefik.docker.network=traefik + # HTTP configuration + - traefik.http.routers.gitlab.entrypoints=web,websecure + - traefik.http.routers.gitlab.tls=true + - traefik.http.routers.gitlab.tls.certresolver=le + - traefik.http.routers.gitlab.rule=Host(`${GITLAB_DOMAIN}`) + - traefik.http.services.gitlab.loadbalancer.server.port=80 + # TCP/SSH configuration - completely revised + - "traefik.tcp.routers.gitlab-ssh.rule=HostSNI(`*`)" + - "traefik.tcp.routers.gitlab-ssh.entrypoints=gitlab-ssh" + - "traefik.tcp.services.gitlab-ssh.loadbalancer.server.port=22" + volumes: + - 'gitlab-config:/etc/gitlab' + - 'gitlab-logs:/var/log/gitlab' + - 'gitlab-data:/var/opt/gitlab' + shm_size: '256m' + networks: + - traefik + restart: unless-stopped + + +volumes: + gitlab-config: + name: gitlab-config + gitlab-logs: + name: gitlab-logs + gitlab-data: + name: gitlab-data + +networks: + traefik: + name: traefik + external: true diff --git a/hedgedoc/.example-env b/hedgedoc/.example-env new file mode 100644 index 0000000..0f63d53 --- /dev/null +++ b/hedgedoc/.example-env @@ -0,0 +1,8 @@ +HEDGEDOC_DOMAIN= +HEDGEDOC_DB_USER= +HEDGEDOC_DB_PASSWORD= +HEDGEDOC_DB_NAME= +HEDGEDOC_DB_HOST= +HEDGEDOC_DB_PORT= +HEDGEDOC_USERNAME= +HEDGEDOC_USER_PASSWORD= diff --git a/hedgedoc/create_infra.bash b/hedgedoc/create_infra.bash new file mode 100755 index 0000000..7460808 --- /dev/null +++ b/hedgedoc/create_infra.bash @@ -0,0 +1,8 @@ +#!/bin/bash + +source .env +source ../core/.env + +docker exec postgres psql -U $POSTGRES_USER -d postgres -c "CREATE USER $HEDGEDOC_DB_USER WITH PASSWORD '$HEDGEDOC_DB_PASSWORD';" +docker exec postgres psql -U $POSTGRES_USER -d postgres -c "CREATE DATABASE $HEDGEDOC_DB_NAME OWNER $HEDGEDOC_DB_USER;" +docker exec postgres psql -U $POSTGRES_USER -d $HEDGEDOC_DB_NAME -c "GRANT ALL PRIVILEGES ON DATABASE $HEDGEDOC_DB_NAME TO $HEDGEDOC_DB_USER;" diff --git a/hedgedoc/docker-compose.yml b/hedgedoc/docker-compose.yml new file mode 100644 index 0000000..5c4a1cc --- /dev/null +++ b/hedgedoc/docker-compose.yml @@ -0,0 +1,42 @@ +services: + hedgedoc: + # Make sure to use the latest release from https://hedgedoc.org/latest-release + image: quay.io/hedgedoc/hedgedoc:1.10.2 + container_name: hedgedoc + environment: + - CMD_DB_URL=postgres://${HEDGEDOC_DB_USER}:${HEDGEDOC_DB_PASSWORD}@${HEDGEDOC_DB_HOST}:${HEDGEDOC_DB_PORT}/${HEDGEDOC_DB_NAME} + - CMD_DOMAIN=${HEDGEDOC_DOMAIN} + - CMD_URL_ADDPORT=false + - CMD_PROTOCOL_USESSL=true + - CMD_ALLOW_EMAIL_REGISTER=false + - CMD_HOST=0.0.0.0 + - CMD_PORT=3000 + - CMD_HSTS_ENABLE=true + labels: + - traefik.enable=true + - traefik.docker.network=traefik + - traefik.http.routers.hedgedoc.rule=Host(`${HEDGEDOC_DOMAIN}`) + - traefik.http.routers.hedgedoc.entrypoints=web,websecure + - traefik.http.routers.hedgedoc.middlewares=https-redirect + - traefik.http.routers.hedgedoc.tls=true + - traefik.http.routers.hedgedoc.tls.certresolver=le + - traefik.http.routers.hedgedoc.service=hedgedoc + - traefik.http.services.hedgedoc.loadbalancer.server.port=3000 + - traefik.http.middlewares.hedgedoc-websocket.headers.customrequestheaders.X-Forwarded-Proto=https + volumes: + - hedgedoc-uploads:/hedgedoc/public/uploads + networks: + - traefik + - database + +volumes: + hedgedoc-uploads: + name: hedgedoc-uploads + +networks: + traefik: + name: traefik + external: true + database: + name: database + external: true diff --git a/mailcow/.editorconfig b/mailcow/.editorconfig new file mode 100644 index 0000000..04ba039 --- /dev/null +++ b/mailcow/.editorconfig @@ -0,0 +1,20 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] + +# Change these settings to your own preference +indent_style = space +indent_size = 2 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/mailcow/.gitignore b/mailcow/.gitignore new file mode 100644 index 0000000..c225dc0 --- /dev/null +++ b/mailcow/.gitignore @@ -0,0 +1,77 @@ +!data/conf/nginx/dynmaps.conf +!data/conf/nginx/meta_exporter.conf +!data/conf/nginx/site.conf +!/**/.gitkeep +*.iml +.idea +.vscode/* +data/assets/ssl-example/* +data/assets/ssl/* +data/conf/borgmatic/ +data/conf/clamav/whitelist.ign2 +data/conf/dovecot/acl_anyone +data/conf/dovecot/dovecot-master.passwd +data/conf/dovecot/dovecot-master.userdb +data/conf/dovecot/extra.conf +data/conf/dovecot/mail_replica.conf +data/conf/dovecot/global_sieve_* +data/conf/dovecot/last_login +data/conf/dovecot/lua +data/conf/dovecot/mail_plugins* +data/conf/dovecot/shared_namespace.conf +data/conf/dovecot/sni.conf +data/conf/dovecot/sogo-sso.conf +data/conf/dovecot/sogo_trusted_ip.conf +data/conf/dovecot/sql +data/conf/dovecot/conf.d/fts.conf +data/conf/nextcloud-*.bak +data/conf/nginx/*.active +data/conf/nginx/*.bak +data/conf/nginx/*.conf +data/conf/nginx/*.custom +data/conf/phpfpm/sogo-sso/sogo-sso.pass +data/conf/portainer/ +data/conf/postfix/allow_mailcow_local.regexp +data/conf/postfix/custom_postscreen_whitelist.cidr +data/conf/postfix/custom_transport.pcre +data/conf/postfix/extra.cf +data/conf/postfix/sni.map +data/conf/postfix/sni.map.db +data/conf/postfix/sql +data/conf/postfix/dns_blocklists.cf +data/conf/postfix/dnsbl_reply.map +data/conf/rspamd/custom/* +data/conf/rspamd/local.d/* +data/conf/rspamd/override.d/* +data/conf/sogo/custom-theme.js +data/conf/sogo/plist_ldap +data/conf/sogo/plist_ldap.sh +data/conf/sogo/sieve.creds +data/conf/sogo/cron.creds +data/conf/sogo/custom-fulllogo.svg +data/conf/sogo/custom-shortlogo.svg +data/conf/sogo/custom-fulllogo.png +data/gitea/ +data/gogs/ +data/hooks/dovecot/* +data/hooks/phpfpm/* +data/hooks/postfix/* +data/hooks/rspamd/* +data/hooks/sogo/* +data/hooks/unbound/* +data/web/templates/cache/* +!data/web/templates/cache/.gitkeep +data/web/.well-known/acme-challenge +data/web/css/build/0081-custom-mailcow.css +data/web/inc/vars.local.inc.php +data/web/inc/app_info.inc.php +data/web/nextcloud*/ +data/web/rc*/ +docker-compose.override.yml +mailcow.conf +mailcow.conf_backup +rebuild-images.sh +refresh_images.sh +update_diffs/ +create_cold_standby.sh +!data/conf/nginx/mailcow_auth.conf diff --git a/mailcow/CODE_OF_CONDUCT.md b/mailcow/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..1430c31 --- /dev/null +++ b/mailcow/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, documentation edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at info@servercow.de. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/mailcow/CONTRIBUTING.md b/mailcow/CONTRIBUTING.md new file mode 100644 index 0000000..fae8f1d --- /dev/null +++ b/mailcow/CONTRIBUTING.md @@ -0,0 +1,58 @@ +# Contribution Guidelines +**_Last modified on 15th August 2024_** + +First of all, thank you for wanting to provide a bugfix or a new feature for the mailcow community, it's because of your help that the project can continue to grow! + +As we want to keep mailcow's development structured we setup these Guidelines which helps you to create your issue/pull request accordingly. + +**PLEASE NOTE, THAT WE MIGHT CLOSE ISSUES/PULL REQUESTS IF THEY DON'T FULLFIL OUR WRITTEN GUIDELINES WRITTEN INSIDE THIS DOCUMENT**. So please check this guidelines before you propose a Issue/Pull Request. + +## Topics + +- [Pull Requests](#pull-requests) +- [Issue Reporting](#issue-reporting) + - [Guidelines](#issue-reporting-guidelines) + - [Issue Report Guide](#issue-report-guide) + +## Pull Requests +**_Last modified on 15th August 2024_** + +However, please note the following regarding pull requests: + +1. **ALWAYS** create your PR using the staging branch of your locally cloned mailcow instance, as the pull request will end up in said staging branch of mailcow once approved. Ideally, you should simply create a new branch for your pull request that is named after the type of your PR (e.g. `feat/` for function updates or `fix/` for bug fixes) and the actual content (e.g. `sogo-6.0.0` for an update from SOGo to version 6 or `html-escape` for a fix that includes escaping HTML in mailcow). +2. **ALWAYS** report/request issues/features in the english language, even though mailcow is a german based company. This is done to allow other GitHub users to reply to your issues/requests too which did not speak german or other languages besides english. +3. Please **keep** this pull request branch **clean** and free of commits that have nothing to do with the changes you have made (e.g. commits from other users from other branches). *If you make changes to the `update.sh` script or other scripts that trigger a commit, there is usually a developer mode for clean working in this case.* +4. **Test your changes before you commit them as a pull request.** If possible, write a small **test log** or demonstrate the functionality with a **screenshot or GIF**. *We will of course also test your pull request ourselves, but proof from you will save us the question of whether you have tested your own changes yourself.* +5. **Please use** the pull request template we provide once creating a pull request. *HINT: During editing you encounter comments which looks like: ``. These can be removed or kept, as they will not rendered later on GitHub! Please only create actual content without the said comments.* +6. Please **ALWAYS** create the actual pull request against the staging branch and **NEVER** directly against the master branch. *If you forget to do this, our moobot will remind you to switch the branch to staging.* +7. Wait for a merge commit: It may happen that we do not accept your pull request immediately or sometimes not at all for various reasons. Please do not be disappointed if this is the case. We always endeavor to incorporate any meaningful changes from the community into the mailcow project. +8. If you are planning larger and therefore more complex pull requests, it would be advisable to first announce this in a separate issue and then start implementing it after the idea has been accepted in order to avoid unnecessary frustration and effort! + +--- + +## Issue Reporting +**_Last modified on 15th August 2024_** + +If you plan to report a issue within mailcow please read and understand the following rules: + +### Issue Reporting Guidelines + +1. **ONLY** use the issue tracker for bug reports or improvement requests and NOT for support questions. For support questions you can either contact the [mailcow community on Telegram](https://docs.mailcow.email/#community-support-and-chat) or the mailcow team directly in exchange for a [support fee](https://docs.mailcow.email/#commercial-support). +2. **ONLY** report an error if you have the **necessary know-how (at least the basics)** for the administration of an e-mail server and the usage of Docker. mailcow is a complex and fully-fledged e-mail server including groupware components on a Docker basement and it requires a bit of technical know-how for debugging and operating. +3. **ALWAYS** report/request issues/features in the english language, even though mailcow is a german based company. This is done to allow other GitHub users to reply to your issues/requests too which did not speak german or other languages besides english. +4. **ONLY** report bugs that are contained in the latest mailcow release series. *The definition of the latest release series includes the last major patch (e.g. 2023-12) and all minor patches (revisions) below it (e.g. 2023-12a, b, c etc.).* New issue reports published starting from January 1, 2024 must meet this criterion, as versions below the latest releases are no longer supported by us. +5. When reporting a problem, please be as detailed as possible and include even the smallest changes to your mailcow installation. Simply fill out the corresponding bug report form in detail and accurately to minimize possible questions. +6. **Before you open an issue/feature request**, please first check whether a similar request already exists in the mailcow tracker on GitHub. If so, please include yourself in this request. +7. When you create a issue/feature request: Please note that the creation does **not guarantee an instant implementation or fix by the mailcow team or the community**. +8. Please **ALWAYS** anonymize any sensitive information in your bug report or feature request before submitting it. + +### Issue Report Guide +1. Read your logs; follow them to see what the reason for your problem is. +2. Follow the leads given to you in your logfiles and start investigating. +3. Restarting the troubled service or the whole stack to see if the problem persists. +4. Read the [documentation](https://docs.mailcow.email/) of the troubled service and search its bugtracker for your problem. +5. Search our [issues](https://github.com/mailcow/mailcow-dockerized/issues) for your problem. +6. [Create an issue](https://github.com/mailcow/mailcow-dockerized/issues/new/choose) over at our GitHub repository if you think your problem might be a bug or a missing feature you badly need. But please make sure, that you include **all the logs** and a full description to your problem. +7. Ask your questions in our community-driven [support channels](https://docs.mailcow.email/#community-support-and-chat). + +## When creating an issue/feature request or a pull request, you will be asked to confirm these guidelines. diff --git a/mailcow/LICENSE b/mailcow/LICENSE new file mode 100644 index 0000000..9cecc1d --- /dev/null +++ b/mailcow/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 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 General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is 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. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + 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. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + 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 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. Use with the GNU Affero General Public License. + + 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 Affero 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 special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 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 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 General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + 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 GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/mailcow/README.md b/mailcow/README.md new file mode 100644 index 0000000..dc9b76b --- /dev/null +++ b/mailcow/README.md @@ -0,0 +1,45 @@ +# mailcow: dockerized - 🮠+ 🋠= 💕 + +[![Translation status](https://translate.mailcow.email/widgets/mailcow-dockerized/-/translation/svg-badge.svg)](https://translate.mailcow.email/engage/mailcow-dockerized/) +[![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/mailcow_email.svg?style=social&label=Follow%20%40mailcow_email)](https://twitter.com/mailcow_email) +![Mastodon Follow](https://img.shields.io/mastodon/follow/109388212176073348?domain=https%3A%2F%2Fmailcow.social&label=Follow%20%40doncow%40mailcow.social&link=https%3A%2F%2Fmailcow.social%2F%40doncow) + + +## Want to support mailcow? + +Please [consider a support contract with Servercow](https://www.servercow.de/mailcow?lang=en#support) to support further development. _We_ support _you_ while _you_ support _us_. :) + +You can also [get a SAL](https://www.servercow.de/mailcow?lang=en#sal) which is a one-time payment with no liabilities or returning fees. + +Or just spread the word: moo. + +## Info, documentation and support + +Please see [the official documentation](https://docs.mailcow.email/) for installation and support instructions. 🄠+ +🛠**If you found a critical security issue, please mail us to [info at servercow.de](mailto:info@servercow.de).** + +## Cowmunity + +[mailcow community](https://community.mailcow.email) + +[Telegram mailcow channel](https://telegram.me/mailcow) + +[Telegram mailcow Off-Topic channel](https://t.me/mailcowOfftopic) + +[Official ð• (Twitter) Account](https://twitter.com/mailcow_email) + +[Official Mastodon Account](https://mailcow.social/@doncow) + +Telegram desktop clients are available for [multiple platforms](https://desktop.telegram.org). You can search the groups history for keywords. + +## Misc + +**Important**: mailcow makes use of various open-source software. Please assure you agree with their license before using mailcow. +Any part of mailcow itself is released under **GNU General Public License, Version 3**. + +mailcow is a registered word mark of The Infrastructure Company GmbH, Parkstr. 42, 47877 Willich, Germany. + +The project is managed and maintained by The Infrastructure Company GmbH. + +Originated from @andryyy (André) diff --git a/mailcow/SECURITY.md b/mailcow/SECURITY.md new file mode 100644 index 0000000..de63ca3 --- /dev/null +++ b/mailcow/SECURITY.md @@ -0,0 +1,42 @@ +# Security Policies and Procedures + +This document outlines security procedures and general policies for the _mailcow: dockerized_ project as found on [mailcow-dockerized](https://github.com/mailcow/mailcow-dockerized). + + * [Reporting a Vulnerability](#reporting-a-vulnerability) + * [Disclosure Policy](#disclosure-policy) + * [Comments on this Policy](#comments-on-this-policy) + +## Reporting a Vulnerability + +The mailcow team and community take all security vulnerabilities +seriously. Thank you for improving the security of our open source +software. We appreciate your efforts and responsible disclosure and will +make every effort to acknowledge your contributions. + +Report security vulnerabilities by emailing the mailcow team at: + + info at servercow.de + +mailcow team will acknowledge your email as soon as possible, and will +send a more detailed response afterwards indicating the next steps in +handling your report. After the initial reply to your report, the mailcow +team will endeavor to keep you informed of the progress towards a fix and +full announcement, and may ask for additional information or guidance. + +Report security vulnerabilities in third-party modules to the person or +team maintaining the module. + +## Disclosure Policy + +When the mailcow team receives a security bug report, they will assign it +to a primary handler. This person will coordinate the fix and release +process, involving the following steps: + + * Confirm the problem and determine the affected versions. + * Audit code to find any potential similar problems. + * Prepare fixes for all releases still under maintenance. + +## Comments on this Policy + +If you have suggestions on how this process could be improved please submit a +pull request. diff --git a/mailcow/data/Dockerfiles/acme/Dockerfile b/mailcow/data/Dockerfiles/acme/Dockerfile new file mode 100644 index 0000000..f6e990e --- /dev/null +++ b/mailcow/data/Dockerfiles/acme/Dockerfile @@ -0,0 +1,27 @@ +FROM alpine:3.21 + +LABEL maintainer = "The Infrastructure Company GmbH " + +RUN apk upgrade --no-cache \ + && apk add --update --no-cache \ + bash \ + curl \ + openssl \ + bind-tools \ + jq \ + mariadb-client \ + redis \ + tini \ + tzdata \ + python3 \ + acme-tiny + +COPY acme.sh /srv/acme.sh +COPY functions.sh /srv/functions.sh +COPY obtain-certificate.sh /srv/obtain-certificate.sh +COPY reload-configurations.sh /srv/reload-configurations.sh +COPY expand6.sh /srv/expand6.sh + +RUN chmod +x /srv/*.sh + +CMD ["/sbin/tini", "-g", "--", "/srv/acme.sh"] diff --git a/mailcow/data/Dockerfiles/acme/acme.sh b/mailcow/data/Dockerfiles/acme/acme.sh new file mode 100755 index 0000000..a6766ef --- /dev/null +++ b/mailcow/data/Dockerfiles/acme/acme.sh @@ -0,0 +1,431 @@ +#!/bin/bash +set -o pipefail +exec 5>&1 + +# Do not attempt to write to slave +if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then + export REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning" +else + export REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning" +fi + +until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do + echo "Waiting for Redis..." + sleep 2 +done + +source /srv/functions.sh +# Thanks to https://github.com/cvmiller -> https://github.com/cvmiller/expand6 +source /srv/expand6.sh + +# Skipping IP check when we like to live dangerously +if [[ "${SKIP_IP_CHECK}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + SKIP_IP_CHECK=y +fi + +# Skipping HTTP check when we like to live dangerously +if [[ "${SKIP_HTTP_VERIFICATION}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + SKIP_HTTP_VERIFICATION=y +fi + +# Request certificate for MAILCOW_HOSTNAME only +if [[ "${ONLY_MAILCOW_HOSTNAME}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + ONLY_MAILCOW_HOSTNAME=y +fi + +if [[ "${AUTODISCOVER_SAN}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + AUTODISCOVER_SAN=y +fi + +# Request individual certificate for every domain +if [[ "${ENABLE_SSL_SNI}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + ENABLE_SSL_SNI=y +fi + +if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + log_f "SKIP_LETS_ENCRYPT=y, skipping Let's Encrypt..." + sleep 365d + exec $(readlink -f "$0") +fi + +log_f "Waiting for Docker API..." +until ping dockerapi -c1 > /dev/null; do + sleep 1 +done +log_f "Docker API OK" + +log_f "Waiting for Postfix..." +until ping postfix -c1 > /dev/null; do + sleep 1 +done +log_f "Postfix OK" + +log_f "Waiting for Dovecot..." +until ping dovecot -c1 > /dev/null; do + sleep 1 +done +log_f "Dovecot OK" + +ACME_BASE=/var/lib/acme +SSL_EXAMPLE=/var/lib/ssl-example + +mkdir -p ${ACME_BASE}/acme + +# Migrate +[[ -f ${ACME_BASE}/acme/private/privkey.pem ]] && mv ${ACME_BASE}/acme/private/privkey.pem ${ACME_BASE}/acme/key.pem +[[ -f ${ACME_BASE}/acme/private/account.key ]] && mv ${ACME_BASE}/acme/private/account.key ${ACME_BASE}/acme/account.pem +if [[ -f ${ACME_BASE}/acme/key.pem && -f ${ACME_BASE}/acme/cert.pem ]]; then + if verify_hash_match ${ACME_BASE}/acme/cert.pem ${ACME_BASE}/acme/key.pem; then + log_f "Migrating to SNI folder structure..." + CERT_DOMAIN=($(openssl x509 -noout -text -in ${ACME_BASE}/acme/cert.pem | grep "Subject:" | sed -e 's/\(Subject:\)\|\(CN = \)\|\(CN=\)//g' | sed -e 's/^[[:space:]]*//')) + CERT_DOMAINS=(${CERT_DOMAIN} $(openssl x509 -noout -text -in ${ACME_BASE}/acme/cert.pem | grep "DNS:" | sed -e 's/\(DNS:\)\|,//g' | sed "s/${CERT_DOMAIN}//" | sed -e 's/^[[:space:]]*//')) + mkdir -p ${ACME_BASE}/${CERT_DOMAIN} + mv ${ACME_BASE}/acme/cert.pem ${ACME_BASE}/${CERT_DOMAIN}/cert.pem + # key is only copied, not moved, because it is used by all other requests too + cp ${ACME_BASE}/acme/key.pem ${ACME_BASE}/${CERT_DOMAIN}/key.pem + chmod 600 ${ACME_BASE}/${CERT_DOMAIN}/key.pem + echo -n ${CERT_DOMAINS[*]} > ${ACME_BASE}/${CERT_DOMAIN}/domains + mv ${ACME_BASE}/acme/acme.csr ${ACME_BASE}/${CERT_DOMAIN}/acme.csr + log_f "OK" no_date + fi +fi + +[[ ! -f ${ACME_BASE}/dhparams.pem ]] && cp ${SSL_EXAMPLE}/dhparams.pem ${ACME_BASE}/dhparams.pem + +if [[ -f ${ACME_BASE}/cert.pem ]] && [[ -f ${ACME_BASE}/key.pem ]] && [[ $(stat -c%s ${ACME_BASE}/cert.pem) != 0 ]]; then + ISSUER=$(openssl x509 -in ${ACME_BASE}/cert.pem -noout -issuer) + if [[ ${ISSUER} != *"Let's Encrypt"* && ${ISSUER} != *"mailcow"* && ${ISSUER} != *"Fake LE Intermediate"* ]]; then + log_f "Found certificate with issuer other than mailcow snake-oil CA and Let's Encrypt, skipping ACME client..." + sleep 3650d + exec $(readlink -f "$0") + fi +else + if [[ -f ${ACME_BASE}/${MAILCOW_HOSTNAME}/cert.pem ]] && [[ -f ${ACME_BASE}/${MAILCOW_HOSTNAME}/key.pem ]] && verify_hash_match ${ACME_BASE}/${MAILCOW_HOSTNAME}/cert.pem ${ACME_BASE}/${MAILCOW_HOSTNAME}/key.pem; then + log_f "Restoring previous acme certificate and restarting script..." + cp ${ACME_BASE}/${MAILCOW_HOSTNAME}/cert.pem ${ACME_BASE}/cert.pem + cp ${ACME_BASE}/${MAILCOW_HOSTNAME}/key.pem ${ACME_BASE}/key.pem + # Restarting with env var set to trigger a restart, + exec env TRIGGER_RESTART=1 $(readlink -f "$0") + else + log_f "Restoring mailcow snake-oil certificates and restarting script..." + cp ${SSL_EXAMPLE}/cert.pem ${ACME_BASE}/cert.pem + cp ${SSL_EXAMPLE}/key.pem ${ACME_BASE}/key.pem + exec env TRIGGER_RESTART=1 $(readlink -f "$0") + fi +fi + +chmod 600 ${ACME_BASE}/key.pem + +log_f "Waiting for database..." +while ! /usr/bin/mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent > /dev/null; do + sleep 2 +done +log_f "Database OK" + +log_f "Waiting for Nginx..." +until $(curl --output /dev/null --silent --head --fail http://nginx.${COMPOSE_PROJECT_NAME}_mailcow-network:8081); do + sleep 2 +done +log_f "Nginx OK" + +log_f "Waiting for resolver..." +until dig letsencrypt.org +time=3 +tries=1 @unbound > /dev/null; do + sleep 2 +done +log_f "Resolver OK" + +# Waiting for domain table +log_f "Waiting for domain table..." +while [[ -z ${DOMAIN_TABLE} ]]; do + curl --silent http://nginx.${COMPOSE_PROJECT_NAME}_mailcow-network/ >/dev/null 2>&1 + DOMAIN_TABLE=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'domain'" -Bs) + [[ -z ${DOMAIN_TABLE} ]] && sleep 10 +done +log_f "OK" no_date + +log_f "Initializing, please wait..." + +while true; do + POSTFIX_CERT_SERIAL="$(echo | openssl s_client -connect postfix:25 -starttls smtp 2>/dev/null | openssl x509 -inform pem -noout -serial | cut -d "=" -f 2)" + DOVECOT_CERT_SERIAL="$(echo | openssl s_client -connect dovecot:143 -starttls imap 2>/dev/null | openssl x509 -inform pem -noout -serial | cut -d "=" -f 2)" + POSTFIX_CERT_SERIAL_NEW="$(echo | openssl s_client -connect postfix:25 -starttls smtp 2>/dev/null | openssl x509 -inform pem -noout -serial | cut -d "=" -f 2)" + DOVECOT_CERT_SERIAL_NEW="$(echo | openssl s_client -connect dovecot:143 -starttls imap 2>/dev/null | openssl x509 -inform pem -noout -serial | cut -d "=" -f 2)" + # Re-using previous acme-mailcow account and domain keys + if [[ ! -f ${ACME_BASE}/acme/key.pem ]]; then + log_f "Generating missing domain private rsa key..." + openssl genrsa 4096 > ${ACME_BASE}/acme/key.pem + else + log_f "Using existing domain rsa key ${ACME_BASE}/acme/key.pem" + fi + if [[ ! -f ${ACME_BASE}/acme/account.pem ]]; then + log_f "Generating missing Lets Encrypt account key..." + if [[ ! -z ${ACME_CONTACT} ]]; then + if ! verify_email "${ACME_CONTACT}"; then + log_f "Invalid email address, will not start registration!" + sleep 365d + exec $(readlink -f "$0") + else + ACME_CONTACT_PARAMETER="--contact mailto:${ACME_CONTACT}" + log_f "Valid email address, using ${ACME_CONTACT} for registration" + fi + else + ACME_CONTACT_PARAMETER="" + fi + openssl genrsa 4096 > ${ACME_BASE}/acme/account.pem + else + log_f "Using existing Lets Encrypt account key ${ACME_BASE}/acme/account.pem" + fi + + chmod 600 ${ACME_BASE}/acme/key.pem + chmod 600 ${ACME_BASE}/acme/account.pem + + unset EXISTING_CERTS + declare -a EXISTING_CERTS + for cert_dir in ${ACME_BASE}/*/ ; do + if [[ ! -f ${cert_dir}domains ]] || [[ ! -f ${cert_dir}cert.pem ]] || [[ ! -f ${cert_dir}key.pem ]]; then + continue + fi + EXISTING_CERTS+=("$(basename ${cert_dir})") + done + + # Cleaning up and init validation arrays + unset SQL_DOMAIN_ARR + unset VALIDATED_CONFIG_DOMAINS + unset ADDITIONAL_VALIDATED_SAN + unset ADDITIONAL_WC_ARR + unset ADDITIONAL_SAN_ARR + unset CERT_ERRORS + unset CERT_CHANGED + unset CERT_AMOUNT_CHANGED + unset VALIDATED_CERTIFICATES + CERT_ERRORS=0 + CERT_CHANGED=0 + CERT_AMOUNT_CHANGED=0 + declare -a SQL_DOMAIN_ARR + declare -a VALIDATED_CONFIG_DOMAINS + declare -a ADDITIONAL_VALIDATED_SAN + declare -a ADDITIONAL_WC_ARR + declare -a ADDITIONAL_SAN_ARR + declare -a VALIDATED_CERTIFICATES + IFS=',' read -r -a TMP_ARR <<< "${ADDITIONAL_SAN}" + for i in "${TMP_ARR[@]}" ; do + if [[ "$i" =~ \.\*$ ]]; then + ADDITIONAL_WC_ARR+=(${i::-2}) + else + ADDITIONAL_SAN_ARR+=($i) + fi + done + + if [[ ${AUTODISCOVER_SAN} == "y" ]]; then + # Fetch certs for autoconfig and autodiscover subdomains + ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig') + fi + + if [[ ${SKIP_IP_CHECK} != "y" ]]; then + # Start IP detection + log_f "Detecting IP addresses..." + IPV4=$(get_ipv4) + IPV6=$(get_ipv6) + log_f "OK: ${IPV4}, ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}" + fi + + ######################################### + # IP and webroot challenge verification # + SQL_DOMAINS=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain WHERE backupmx=0 and active=1" -Bs) + if [[ ! $? -eq 0 ]]; then + log_f "Failed to read SQL domains, retrying in 1 minute..." + sleep 1m + exec $(readlink -f "$0") + fi + while read domains; do + if [[ -z "${domains}" ]]; then + # ignore empty lines + continue + fi + SQL_DOMAIN_ARR+=("${domains}") + done <<< "${SQL_DOMAINS}" + + if [[ ${ONLY_MAILCOW_HOSTNAME} != "y" ]]; then + for SQL_DOMAIN in "${SQL_DOMAIN_ARR[@]}"; do + unset VALIDATED_CONFIG_DOMAINS_SUBDOMAINS + declare -a VALIDATED_CONFIG_DOMAINS_SUBDOMAINS + for SUBDOMAIN in "${ADDITIONAL_WC_ARR[@]}"; do + if [[ "${SUBDOMAIN}.${SQL_DOMAIN}" != "${MAILCOW_HOSTNAME}" ]]; then + if check_domain "${SUBDOMAIN}.${SQL_DOMAIN}"; then + VALIDATED_CONFIG_DOMAINS_SUBDOMAINS+=("${SUBDOMAIN}.${SQL_DOMAIN}") + fi + fi + done + VALIDATED_CONFIG_DOMAINS+=("${VALIDATED_CONFIG_DOMAINS_SUBDOMAINS[*]}") + done + fi + + if check_domain ${MAILCOW_HOSTNAME}; then + VALIDATED_MAILCOW_HOSTNAME="${MAILCOW_HOSTNAME}" + fi + + if [[ ${ONLY_MAILCOW_HOSTNAME} != "y" ]]; then + for SAN in "${ADDITIONAL_SAN_ARR[@]}"; do + # Skip on CAA errors for SAN + SAN_PARENT_DOMAIN=$(echo ${SAN} | cut -d. -f2-) + SAN_CAAS=( $(dig CAA ${SAN_PARENT_DOMAIN} +short | sed -n 's/\d issue "\(.*\)"/\1/p') ) + if [[ ! -z ${SAN_CAAS} ]]; then + if [[ ${SAN_CAAS[@]} =~ "letsencrypt.org" ]]; then + log_f "Validated CAA for parent domain ${SAN_PARENT_DOMAIN} of ${SAN}" + else + log_f "Skipping ACME validation for ${SAN}: Lets Encrypt disallowed for ${SAN} by CAA record" + continue + fi + fi + if [[ ${SAN} == ${MAILCOW_HOSTNAME} ]]; then + continue + fi + if check_domain ${SAN}; then + ADDITIONAL_VALIDATED_SAN+=("${SAN}") + fi + done + fi + + # Unique domains for server certificate + if [[ ${ENABLE_SSL_SNI} == "y" ]]; then + # create certificate for server name and fqdn SANs only + SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs)) + else + # create certificate for all domains, including all subdomains from other domains [*] + SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs)) + fi + if [[ ! -z ${SERVER_SAN_VALIDATED[*]} ]]; then + CERT_NAME=${SERVER_SAN_VALIDATED[0]} + VALIDATED_CERTIFICATES+=("${CERT_NAME}") + + # obtain server certificate if required + ACME_CONTACT_PARAMETER=${ACME_CONTACT_PARAMETER} DOMAINS=${SERVER_SAN_VALIDATED[@]} /srv/obtain-certificate.sh rsa + RETURN="$?" + if [[ "$RETURN" == "0" ]]; then # 0 = cert created successfully + CERT_AMOUNT_CHANGED=1 + CERT_CHANGED=1 + elif [[ "$RETURN" == "1" ]]; then # 1 = cert renewed successfully + CERT_CHANGED=1 + elif [[ "$RETURN" == "2" ]]; then # 2 = cert not due for renewal + : + else + CERT_ERRORS=1 + fi + # copy hostname certificate to default/server certificate + # do not a key when cert is missing, this can lead to a mismatch of cert/key + if [[ -f ${ACME_BASE}/${CERT_NAME}/cert.pem ]]; then + cp ${ACME_BASE}/${CERT_NAME}/cert.pem ${ACME_BASE}/cert.pem + cp ${ACME_BASE}/${CERT_NAME}/key.pem ${ACME_BASE}/key.pem + fi + fi + + # individual certificates for SNI [@] + if [[ ${ENABLE_SSL_SNI} == "y" ]]; then + for VALIDATED_DOMAINS in "${VALIDATED_CONFIG_DOMAINS[@]}"; do + VALIDATED_DOMAINS_ARR=(${VALIDATED_DOMAINS}) + + unset VALIDATED_DOMAINS_SORTED + declare -a VALIDATED_DOMAINS_SORTED + VALIDATED_DOMAINS_SORTED=(${VALIDATED_DOMAINS_ARR[0]} $(echo ${VALIDATED_DOMAINS_ARR[@]:1} | xargs -n1 | sort -u | xargs)) + + # remove all domain names that are already inside the server certificate (SERVER_SAN_VALIDATED) + for domain in "${SERVER_SAN_VALIDATED[@]}"; do + for i in "${!VALIDATED_DOMAINS_SORTED[@]}"; do + if [[ ${VALIDATED_DOMAINS_SORTED[i]} = $domain ]]; then + unset 'VALIDATED_DOMAINS_SORTED[i]' + fi + done + done + + if [[ ! -z ${VALIDATED_DOMAINS_SORTED[*]} ]]; then + CERT_NAME=${VALIDATED_DOMAINS_SORTED[0]} + VALIDATED_CERTIFICATES+=("${CERT_NAME}") + # obtain certificate if required + DOMAINS=${VALIDATED_DOMAINS_SORTED[@]} /srv/obtain-certificate.sh rsa + RETURN="$?" + if [[ "$RETURN" == "0" ]]; then # 0 = cert created successfully + CERT_AMOUNT_CHANGED=1 + CERT_CHANGED=1 + elif [[ "$RETURN" == "1" ]]; then # 1 = cert renewed successfully + CERT_CHANGED=1 + elif [[ "$RETURN" == "2" ]]; then # 2 = cert not due for renewal + : + else + CERT_ERRORS=1 + fi + fi + done + fi + + if [[ -z ${VALIDATED_CERTIFICATES[*]} ]]; then + log_f "Cannot validate any hostnames, skipping Let's Encrypt for 1 hour." + log_f "Use SKIP_LETS_ENCRYPT=y in mailcow.conf to skip it permanently." + ${REDIS_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)" + sleep 1h + exec $(readlink -f "$0") + fi + + # find orphaned certificates if no errors occurred + if [[ "${CERT_ERRORS}" == "0" ]]; then + for EXISTING_CERT in "${EXISTING_CERTS[@]}"; do + if [[ ! "`printf '_%s_\n' "${VALIDATED_CERTIFICATES[@]}"`" == *"_${EXISTING_CERT}_"* ]]; then + DATE=$(date +%Y-%m-%d_%H_%M_%S) + log_f "Found orphaned certificate: ${EXISTING_CERT} - archiving it at ${ACME_BASE}/backups/${EXISTING_CERT}/" + BACKUP_DIR=${ACME_BASE}/backups/${EXISTING_CERT}/${DATE} + # archive rsa cert and any other files + mkdir -p ${ACME_BASE}/backups/${EXISTING_CERT} + mv ${ACME_BASE}/${EXISTING_CERT} ${BACKUP_DIR} + CERT_CHANGED=1 + CERT_AMOUNT_CHANGED=1 + fi + done + fi + + # reload on new or changed certificates + if [[ "${CERT_CHANGED}" == "1" ]]; then + rm -f "${ACME_BASE}/force_renew" 2> /dev/null + RELOAD_LOOP_C=1 + while [[ "${POSTFIX_CERT_SERIAL}" == "${POSTFIX_CERT_SERIAL_NEW}" ]] || [[ "${DOVECOT_CERT_SERIAL}" == "${DOVECOT_CERT_SERIAL_NEW}" ]] || [[ ${#POSTFIX_CERT_SERIAL_NEW} -ne 36 ]] || [[ ${#DOVECOT_CERT_SERIAL_NEW} -ne 36 ]]; do + log_f "Reloading or restarting services... (${RELOAD_LOOP_C})" + RELOAD_LOOP_C=$((RELOAD_LOOP_C + 1)) + CERT_AMOUNT_CHANGED=${CERT_AMOUNT_CHANGED} /srv/reload-configurations.sh + log_f "Waiting for containers to settle..." + sleep 10 + until nc -z dovecot 143; do + sleep 1 + done + until nc -z postfix 25; do + sleep 1 + done + POSTFIX_CERT_SERIAL_NEW="$(echo | openssl s_client -connect postfix:25 -starttls smtp 2>/dev/null | openssl x509 -inform pem -noout -serial | cut -d "=" -f 2)" + DOVECOT_CERT_SERIAL_NEW="$(echo | openssl s_client -connect dovecot:143 -starttls imap 2>/dev/null | openssl x509 -inform pem -noout -serial | cut -d "=" -f 2)" + if [[ ${RELOAD_LOOP_C} -gt 3 ]]; then + log_f "Some services do return old end dates, something went wrong!" + ${REDIS_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)" + break; + fi + done + fi + + case "$CERT_ERRORS" in + 0) # all successful + if [[ "${CERT_CHANGED}" == "1" ]]; then + if [[ "${CERT_AMOUNT_CHANGED}" == "1" ]]; then + log_f "Certificates successfully requested and renewed where required, sleeping one day" + else + log_f "Certificates were successfully renewed where required, sleeping for another day." + fi + else + log_f "Certificates were successfully validated, no changes or renewals required, sleeping for another day." + fi + sleep 1d + ;; + *) # non-zero + log_f "Some errors occurred, retrying in 30 minutes..." + ${REDIS_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)" + sleep 30m + exec $(readlink -f "$0") + ;; + esac + +done diff --git a/mailcow/data/Dockerfiles/acme/expand6.sh b/mailcow/data/Dockerfiles/acme/expand6.sh new file mode 100755 index 0000000..a781722 --- /dev/null +++ b/mailcow/data/Dockerfiles/acme/expand6.sh @@ -0,0 +1,131 @@ +#!/bin/bash + +################################################################################## +# +# Copyright (C) 2017 Craig Miller +# +# See the file "LICENSE" for information on usage and redistribution +# of this file, and for a DISCLAIMER OF ALL WARRANTIES. +# Distributed under GPLv2 License +# +################################################################################## + + +# IPv6 Address Expansion functions +# +# by Craig Miller 19 Feb 2017 +# +# 16 Nov 2017 v0.93 - added CLI functionality + + +VERSION=0.93 + +empty_addr="0000:0000:0000:0000:0000:0000:0000:0000" +empty_addr_len=${#empty_addr} + +function usage { + echo " $0 - expand compressed IPv6 addresss " + echo " e.g. $0 2001:db8:1:12:123::456 " + echo " " + echo " -t self test" + echo " " + echo " By Craig Miller - Version: $VERSION" + exit 1 + } + +if [ "$1" == "-h" ]; then + #call help + usage +fi + +# +# Expands IPv6 quibble to 4 digits with leading zeros e.g. db8 -> 0db8 +# +# Returns string with expanded quibble + +function expand_quibble() { + addr=$1 + # create array of quibbles + addr_array=(${addr//:/ }) + addr_array_len=${#addr_array[@]} + # step thru quibbles + for ((i=0; i< $addr_array_len ; i++ )) + do + quibble=${addr_array[$i]} + quibble_len=${#quibble} + case $quibble_len in + 1) quibble="000$quibble";; + 2) quibble="00$quibble";; + 3) quibble="0$quibble";; + esac + addr_array[$i]=$quibble + done + # reconstruct addr from quibbles + return_str=${addr_array[*]} + return_str="${return_str// /:}" + echo $return_str +} + +# +# Expands IPv6 address :: format to full zeros +# +# Returns string with expanded address + +function expand() { + if [[ $1 == *"::"* ]]; then + # check for leading zeros on front_addr + if [[ $1 == "::"* ]]; then + front_addr=0 + else + front_addr=$(echo $1 | sed -r 's;([^ ]+)::.*;\1;') + fi + # check for trailing zeros on back_addr + if [[ $1 == *"::" ]]; then + back_addr=0 + else + back_addr=$(echo $1 | sed -r 's;.*::([^ ]+);\1;') + fi + front_addr=$(expand_quibble $front_addr) + back_addr=$(expand_quibble $back_addr) + + new_addr=$empty_addr + front_addr_len=${#front_addr} + back_addr_len=${#back_addr} + # calculate fill needed + num_zeros=$(($empty_addr_len - $front_addr_len - $back_addr_len - 1)) + + #fill_str=${empty_addr[0]:0:$num_zeros} + new_addr="$front_addr:${empty_addr[0]:0:$num_zeros}$back_addr" + + # return expanded address + echo $new_addr + else + # return input with expandd quibbles + expand_quibble $1 + fi +} + +# self test - call with '-t' parameter +if [ "$1" == "-t" ]; then + # add address examples to test + expand fd11::1d70:cf84:18ef:d056 + expand 2a01::1 + expand fe80::f203:8cff:fe3f:f041 + expand 2001:db8:123::5 + expand 2001:470:ebbd:0:f203:8cff:fe3f:f041 + # special cases + expand ::1 + expand fd32:197d:3022:1101:: + exit 1 +fi + +# allow script to be sourced (with no arguements) +if [[ $1 != "" ]]; then + # validate input is an IPv6 address + if [[ $1 == *":"* ]]; then + expand $1 + else + echo "ERROR: unregcognized IPv6 address $1" + exit 1 + fi +fi diff --git a/mailcow/data/Dockerfiles/acme/functions.sh b/mailcow/data/Dockerfiles/acme/functions.sh new file mode 100644 index 0000000..183be01 --- /dev/null +++ b/mailcow/data/Dockerfiles/acme/functions.sh @@ -0,0 +1,132 @@ +#!/bin/bash + +log_f() { + if [[ ${2} == "no_nl" ]]; then + echo -n "$(date) - ${1}" + elif [[ ${2} == "no_date" ]]; then + echo "${1}" + elif [[ ${2} != "redis_only" ]]; then + echo "$(date) - ${1}" + fi + if [[ ${3} == "b64" ]]; then + ${REDIS_CMDLINE} LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"base64,$(printf '%s' "${MAILCOW_HOSTNAME} - ${1}")\"}" > /dev/null + else + ${REDIS_CMDLINE} LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${MAILCOW_HOSTNAME} - ${1}" | \ + tr '%&;$"[]{}-\r\n' ' ')\"}" > /dev/null + fi +} + +verify_email(){ + regex="^(([A-Za-z0-9]+((\.|\-|\_|\+)?[A-Za-z0-9]?)*[A-Za-z0-9]+)|[A-Za-z0-9]+)@(([A-Za-z0-9]+)+((\.|\-|\_)?([A-Za-z0-9]+)+)*)+\.([A-Za-z]{2,})+$" + if [[ $1 =~ ${regex} ]]; then + return 0 + else + return 1 + fi +} + +verify_hash_match(){ + CERT_HASH=$(openssl x509 -in "${1}" -noout -pubkey | openssl md5) + KEY_HASH=$(openssl pkey -in "${2}" -pubout | openssl md5) + if [[ ${CERT_HASH} != ${KEY_HASH} ]]; then + log_f "Certificate and key hashes do not match!" + return 1 + else + log_f "Verified hashes." + return 0 + fi +} + +get_ipv4(){ + local IPV4= + local IPV4_SRCS= + local TRY= + IPV4_SRCS[0]="ip4.mailcow.email" + IPV4_SRCS[1]="ip4.nevondo.com" + until [[ ! -z ${IPV4} ]] || [[ ${TRY} -ge 10 ]]; do + IPV4=$(curl --connect-timeout 3 -m 10 -L4s ${IPV4_SRCS[$RANDOM % ${#IPV4_SRCS[@]} ]} | grep -E "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$") + [[ ! -z ${TRY} ]] && sleep 1 + TRY=$((TRY+1)) + done + echo ${IPV4} +} + +get_ipv6(){ + local IPV6= + local IPV6_SRCS= + local TRY= + IPV6_SRCS[0]="ip6.mailcow.email" + IPV6_SRCS[1]="ip6.nevondo.com" + until [[ ! -z ${IPV6} ]] || [[ ${TRY} -ge 10 ]]; do + IPV6=$(curl --connect-timeout 3 -m 10 -L6s ${IPV6_SRCS[$RANDOM % ${#IPV6_SRCS[@]} ]} | grep "^\([0-9a-fA-F]\{0,4\}:\)\{1,7\}[0-9a-fA-F]\{0,4\}$") + [[ ! -z ${TRY} ]] && sleep 1 + TRY=$((TRY+1)) + done + echo ${IPV6} +} + +check_domain(){ + DOMAIN=$1 + A_DOMAIN=$(dig A ${DOMAIN} +short | tail -n 1) + AAAA_DOMAIN=$(dig AAAA ${DOMAIN} +short | tail -n 1) + # Hard-fail on CAA errors for MAILCOW_HOSTNAME + PARENT_DOMAIN=$(echo ${DOMAIN} | cut -d. -f2-) + CAAS=( $(dig CAA ${PARENT_DOMAIN} +short | sed -n 's/\d issue "\(.*\)"/\1/p') ) + if [[ ! -z ${CAAS} ]]; then + if [[ ${CAAS[@]} =~ "letsencrypt.org" ]]; then + log_f "Validated CAA for parent domain ${PARENT_DOMAIN}" + else + log_f "Lets Encrypt disallowed for ${PARENT_DOMAIN} by CAA record" + return 1 + fi + fi + # Check if CNAME without v6 enabled target + if [[ ! -z ${AAAA_DOMAIN} ]] && [[ -z $(echo ${AAAA_DOMAIN} | grep "^\([0-9a-fA-F]\{0,4\}:\)\{1,7\}[0-9a-fA-F]\{0,4\}$") ]]; then + AAAA_DOMAIN= + fi + if [[ ! -z ${AAAA_DOMAIN} ]]; then + log_f "Found AAAA record for ${DOMAIN}: ${AAAA_DOMAIN} - skipping A record check" + if [[ $(expand ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}) == $(expand ${AAAA_DOMAIN}) ]] || [[ ${SKIP_IP_CHECK} == "y" ]] || [[ ${SNAT6_TO_SOURCE} != "n" ]]; then + if verify_challenge_path "${DOMAIN}" 6; then + log_f "Confirmed AAAA record with IP $(expand ${AAAA_DOMAIN})" + return 0 + else + log_f "Confirmed AAAA record with IP $(expand ${AAAA_DOMAIN}), but HTTP validation failed" + fi + else + log_f "Cannot match your IP $(expand ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}) against hostname ${DOMAIN} (DNS returned $(expand ${AAAA_DOMAIN}))" + fi + elif [[ ! -z ${A_DOMAIN} ]]; then + log_f "Found A record for ${DOMAIN}: ${A_DOMAIN}" + if [[ ${IPV4:-ERR} == ${A_DOMAIN} ]] || [[ ${SKIP_IP_CHECK} == "y" ]] || [[ ${SNAT_TO_SOURCE} != "n" ]]; then + if verify_challenge_path "${DOMAIN}" 4; then + log_f "Confirmed A record ${A_DOMAIN}" + return 0 + else + log_f "Confirmed A record with IP ${A_DOMAIN}, but HTTP validation failed" + fi + else + log_f "Cannot match your IP ${IPV4} against hostname ${DOMAIN} (DNS returned ${A_DOMAIN})" + fi + else + log_f "No A or AAAA record found for hostname ${DOMAIN}" + fi + return 1 +} + +verify_challenge_path(){ + if [[ ${SKIP_HTTP_VERIFICATION} == "y" ]]; then + echo '(skipping check, returning 0)' + return 0 + fi + # verify_challenge_path URL 4|6 + RANDOM_N=${RANDOM}${RANDOM}${RANDOM} + echo ${RANDOM_N} > /var/www/acme/${RANDOM_N} + if [[ "$(curl --insecure -${2} -L http://${1}/.well-known/acme-challenge/${RANDOM_N} --silent)" == "${RANDOM_N}" ]]; then + rm /var/www/acme/${RANDOM_N} + return 0 + else + rm /var/www/acme/${RANDOM_N} + return 1 + fi +} diff --git a/mailcow/data/Dockerfiles/acme/obtain-certificate.sh b/mailcow/data/Dockerfiles/acme/obtain-certificate.sh new file mode 100644 index 0000000..16c4e25 --- /dev/null +++ b/mailcow/data/Dockerfiles/acme/obtain-certificate.sh @@ -0,0 +1,130 @@ +#!/bin/bash + +# Return values / exit codes +# 0 = cert created successfully +# 1 = cert renewed successfully +# 2 = cert not due for renewal +# * = errors + + +source /srv/functions.sh + +CERT_DOMAINS=(${DOMAINS[@]}) +CERT_DOMAIN=${CERT_DOMAINS[0]} +ACME_BASE=/var/lib/acme + +TYPE=${1} +PREFIX="" +# only support rsa certificates for now +if [[ "${TYPE}" != "rsa" ]]; then + log_f "Unknown certificate type '${TYPE}' requested" + exit 5 +fi +DOMAINS_FILE=${ACME_BASE}/${CERT_DOMAIN}/domains +CERT=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}cert.pem +SHARED_KEY=${ACME_BASE}/acme/${PREFIX}key.pem # must already exist +KEY=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}key.pem +CSR=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}acme.csr + +if [[ -z ${CERT_DOMAINS[*]} ]]; then + log_f "Missing CERT_DOMAINS to obtain a certificate" + exit 3 +fi + +if [[ "${LE_STAGING}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + if [[ ! -z "${DIRECTORY_URL}" ]]; then + log_f "Cannot use DIRECTORY_URL with LE_STAGING=y - ignoring DIRECTORY_URL" + fi + log_f "Using Let's Encrypt staging servers" + DIRECTORY_URL='--directory-url https://acme-staging-v02.api.letsencrypt.org/directory' +elif [[ ! -z "${DIRECTORY_URL}" ]]; then + log_f "Using custom directory URL ${DIRECTORY_URL}" + DIRECTORY_URL="--directory-url ${DIRECTORY_URL}" +fi + +if [[ -f ${DOMAINS_FILE} && "$(cat ${DOMAINS_FILE})" == "${CERT_DOMAINS[*]}" ]]; then + if [[ ! -f ${CERT} || ! -f "${KEY}" || -f "${ACME_BASE}/force_renew" ]]; then + log_f "Certificate ${CERT} doesn't exist yet or forced renewal - start obtaining" + # Certificate exists and did not change but could be due for renewal (30 days) + elif ! openssl x509 -checkend 2592000 -noout -in ${CERT} > /dev/null; then + log_f "Certificate ${CERT} is due for renewal (< 30 days) - start renewing" + else + log_f "Certificate ${CERT} validation done, neither changed nor due for renewal." + exit 2 + fi +else + log_f "Certificate ${CERT} missing or changed domains '${CERT_DOMAINS[*]}' - start obtaining" +fi + + +# Make backup +if [[ -f ${CERT} ]]; then + DATE=$(date +%Y-%m-%d_%H_%M_%S) + BACKUP_DIR=${ACME_BASE}/backups/${CERT_DOMAIN}/${PREFIX}${DATE} + log_f "Creating backups in ${BACKUP_DIR} ..." + mkdir -p ${BACKUP_DIR}/ + [[ -f ${DOMAINS_FILE} ]] && cp ${DOMAINS_FILE} ${BACKUP_DIR}/ + [[ -f ${CERT} ]] && cp ${CERT} ${BACKUP_DIR}/ + [[ -f ${KEY} ]] && cp ${KEY} ${BACKUP_DIR}/ + [[ -f ${CSR} ]] && cp ${CSR} ${BACKUP_DIR}/ +fi + +mkdir -p ${ACME_BASE}/${CERT_DOMAIN} +if [[ ! -f ${KEY} ]]; then + log_f "Copying shared private key for this certificate..." + cp ${SHARED_KEY} ${KEY} + chmod 600 ${KEY} +fi + +# Generating CSR +printf "[SAN]\nsubjectAltName=" > /tmp/_SAN +printf "DNS:%s," "${CERT_DOMAINS[@]}" >> /tmp/_SAN +sed -i '$s/,$//' /tmp/_SAN +openssl req -new -sha256 -key ${KEY} -subj "/" -reqexts SAN -config <(cat "$(openssl version -d | sed 's/.*"\(.*\)"/\1/g')/openssl.cnf" /tmp/_SAN) > ${CSR} + +# acme-tiny writes info to stderr and ceritifcate to stdout +# The redirects will do the following: +# - redirect stdout to temp certificate file +# - redirect acme-tiny stderr to stdout (logs to variable ACME_RESPONSE) +# - tee stderr to get live output and log to dockerd + +log_f "Checking resolver..." +until dig letsencrypt.org +time=3 +tries=1 @unbound > /dev/null; do + sleep 2 +done +log_f "Resolver OK" +log_f "Using command acme-tiny ${DIRECTORY_URL} ${ACME_CONTACT_PARAMETER} --account-key ${ACME_BASE}/acme/account.pem --disable-check --csr ${CSR} --acme-dir /var/www/acme/" +ACME_RESPONSE=$(acme-tiny ${DIRECTORY_URL} ${ACME_CONTACT_PARAMETER} \ + --account-key ${ACME_BASE}/acme/account.pem \ + --disable-check \ + --csr ${CSR} \ + --acme-dir /var/www/acme/ 2>&1 > /tmp/_cert.pem | tee /dev/fd/5; exit ${PIPESTATUS[0]}) +SUCCESS="$?" +ACME_RESPONSE_B64=$(echo "${ACME_RESPONSE}" | openssl enc -e -A -base64) +log_f "${ACME_RESPONSE_B64}" redis_only b64 +case "$SUCCESS" in + 0) # cert requested + log_f "Deploying certificate ${CERT}..." + # Deploy the new certificate and key + # Moving temp cert to {domain} folder + if verify_hash_match /tmp/_cert.pem ${KEY}; then + RETURN=0 # certificate created + if [[ -f ${CERT} ]]; then + RETURN=1 # certificate renewed + fi + mv -f /tmp/_cert.pem ${CERT} + echo -n ${CERT_DOMAINS[*]} > ${DOMAINS_FILE} + rm /var/www/acme/* 2> /dev/null + log_f "Certificate successfully obtained" + exit ${RETURN} + else + log_f "Certificate was successfully requested, but key and certificate have non-matching hashes, ignoring certificate" + exit 4 + fi + ;; + *) # non-zero is non-fun + log_f "Failed to obtain certificate ${CERT} for domains '${CERT_DOMAINS[*]}'" + redis-cli -h redis -a ${REDISPASS} --no-auth-warning SET ACME_FAIL_TIME "$(date +%s)" + exit 100${SUCCESS} + ;; +esac diff --git a/mailcow/data/Dockerfiles/acme/reload-configurations.sh b/mailcow/data/Dockerfiles/acme/reload-configurations.sh new file mode 100644 index 0000000..8d194b6 --- /dev/null +++ b/mailcow/data/Dockerfiles/acme/reload-configurations.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# Reading container IDs +# Wrapping as array to ensure trimmed content when calling $NGINX etc. +NGINX=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"nginx-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " ")) +DOVECOT=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"dovecot-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " ")) +POSTFIX=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " ")) + +reload_nginx(){ + echo "Reloading Nginx..." + NGINX_RELOAD_RET=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${NGINX}/exec -d '{"cmd":"reload", "task":"nginx"}' --silent -H 'Content-type: application/json' | jq -r .type) + [[ ${NGINX_RELOAD_RET} != 'success' ]] && { echo "Could not reload Nginx, restarting container..."; restart_container ${NGINX} ; } +} + +reload_dovecot(){ + echo "Reloading Dovecot..." + DOVECOT_RELOAD_RET=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${DOVECOT}/exec -d '{"cmd":"reload", "task":"dovecot"}' --silent -H 'Content-type: application/json' | jq -r .type) + [[ ${DOVECOT_RELOAD_RET} != 'success' ]] && { echo "Could not reload Dovecot, restarting container..."; restart_container ${DOVECOT} ; } +} + +reload_postfix(){ + echo "Reloading Postfix..." + POSTFIX_RELOAD_RET=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${POSTFIX}/exec -d '{"cmd":"reload", "task":"postfix"}' --silent -H 'Content-type: application/json' | jq -r .type) + [[ ${POSTFIX_RELOAD_RET} != 'success' ]] && { echo "Could not reload Postfix, restarting container..."; restart_container ${POSTFIX} ; } +} + +restart_container(){ + for container in $*; do + echo "Restarting ${container}..." + C_REST_OUT=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${container}/restart --silent | jq -r '.msg') + echo "${C_REST_OUT}" + done +} + +if [[ "${CERT_AMOUNT_CHANGED}" == "1" ]]; then + restart_container ${NGINX} + restart_container ${DOVECOT} + restart_container ${POSTFIX} +else + reload_nginx + #reload_dovecot + restart_container ${DOVECOT} + #reload_postfix + restart_container ${POSTFIX} +fi diff --git a/mailcow/data/Dockerfiles/backup/Dockerfile b/mailcow/data/Dockerfiles/backup/Dockerfile new file mode 100644 index 0000000..6234e72 --- /dev/null +++ b/mailcow/data/Dockerfiles/backup/Dockerfile @@ -0,0 +1,3 @@ +FROM debian:bookworm-slim + +RUN apt update && apt install pigz -y --no-install-recommends \ No newline at end of file diff --git a/mailcow/data/Dockerfiles/clamd/Dockerfile b/mailcow/data/Dockerfiles/clamd/Dockerfile new file mode 100644 index 0000000..e60e7ee --- /dev/null +++ b/mailcow/data/Dockerfiles/clamd/Dockerfile @@ -0,0 +1,110 @@ +FROM alpine:3.21 AS builder + +WORKDIR /src +ENV CLAMD_VERSION=1.4.2 + +RUN apk upgrade --no-cache \ + && apk add --update --no-cache \ + g++ \ + gcc \ + gdb \ + make \ + cmake \ + py3-pytest \ + python3 \ + valgrind \ + bzip2-dev \ + check-dev \ + curl-dev \ + json-c-dev \ + libmilter-dev \ + libxml2-dev \ + linux-headers \ + ncurses-dev \ + openssl-dev \ + pcre2-dev \ + zlib-dev \ + cargo \ + rust + +RUN wget -P /src https://www.clamav.net/downloads/production/clamav-${CLAMD_VERSION}.tar.gz \ + && tar xzfv /src/clamav-${CLAMD_VERSION}.tar.gz \ + && cd /src/clamav-${CLAMD_VERSION} \ + && cmake . \ + -D CMAKE_BUILD_TYPE="Release" \ + -D CMAKE_INSTALL_PREFIX="/usr" \ + -D CMAKE_INSTALL_LIBDIR="/usr/lib" \ + -D APP_CONFIG_DIRECTORY="/etc/clamav" \ + -D DATABASE_DIRECTORY="/var/lib/clamav" \ + -D ENABLE_CLAMONACC=OFF \ + -D ENABLE_EXAMPLES=OFF \ + -D ENABLE_MILTER=ON \ + -D ENABLE_MAN_PAGES=OFF \ + -D ENABLE_STATIC_LIB=OFF \ + -D ENABLE_JSON_SHARED=ON \ + && cmake --build . \ + && make DESTDIR="/clamav" -j$(($(nproc) - 1)) install \ + && rm -r "/clamav/usr/lib/pkgconfig/" \ + && sed -e "s|^\(Example\)|\# \1|" \ + -e "s|.*\(LocalSocket\) .*|\1 /tmp/clamd.sock|" \ + -e "s|.*\(TCPSocket\) .*|\1 3310|" \ + -e "s|.*\(TCPAddr\) .*|#\1 0.0.0.0|" \ + -e "s|.*\(User\) .*|\1 clamav|" \ + -e "s|^\#\(LogFile\) .*|\1 /var/log/clamav/clamd.log|" \ + -e "s|^\#\(LogTime\).*|\1 yes|" \ + "/clamav/etc/clamav/clamd.conf.sample" > "/clamav/etc/clamav/clamd.conf" \ + && sed -e "s|^\(Example\)|\# \1|" \ + -e "s|.*\(DatabaseOwner\) .*|\1 clamav|" \ + -e "s|^\#\(UpdateLogFile\) .*|\1 /var/log/clamav/freshclam.log|" \ + -e "s|^\#\(NotifyClamd\).*|\1 /etc/clamav/clamd.conf|" \ + -e "s|^\#\(ScriptedUpdates\).*|\1 yes|" \ + "/clamav/etc/clamav/freshclam.conf.sample" > "/clamav/etc/clamav/freshclam.conf" \ + && sed -e "s|^\(Example\)|\# \1|" \ + -e "s|.*\(MilterSocket\) .*|\1 inet:7357|" \ + -e "s|.*\(User\) .*|\1 clamav|" \ + -e "s|^\#\(LogFile\) .*|\1 /var/log/clamav/milter.log|" \ + -e "s|^\#\(LogTime\).*|\1 yes|" \ + -e "s|.*\(\ClamdSocket\) .*|\1 unix:/tmp/clamd.sock|" \ + "/clamav/etc/clamav/clamav-milter.conf.sample" > "/clamav/etc/clamav/clamav-milter.conf" || exit 1 + + +FROM alpine:3.21 + +LABEL maintainer = "The Infrastructure Company GmbH " + +RUN apk upgrade --no-cache \ + && apk add --update --no-cache \ + tzdata \ + rsync \ + bind-tools \ + bash \ + tini \ + json-c \ + libbz2 \ + libcurl \ + libmilter \ + libxml2 \ + ncurses-libs \ + pcre2 \ + zlib \ + libgcc \ + && addgroup -S "clamav" && \ + adduser -D -G "clamav" -h "/var/lib/clamav" -s "/bin/false" -S "clamav" && \ + install -d -m 755 -g "clamav" -o "clamav" "/var/log/clamav" && \ + chown -R clamav:clamav /var/lib/clamav + +COPY --from=builder "/clamav" "/" + +# init +COPY clamd.sh /clamd.sh +RUN chmod +x /sbin/tini + +# healthcheck +COPY healthcheck.sh /healthcheck.sh +COPY clamdcheck.sh /usr/local/bin +RUN chmod +x /healthcheck.sh +RUN chmod +x /usr/local/bin/clamdcheck.sh +HEALTHCHECK --start-period=6m CMD "/healthcheck.sh" + +ENTRYPOINT [] +CMD ["/sbin/tini", "-g", "--", "/clamd.sh"] \ No newline at end of file diff --git a/mailcow/data/Dockerfiles/clamd/clamd.sh b/mailcow/data/Dockerfiles/clamd/clamd.sh new file mode 100755 index 0000000..2c6e75d --- /dev/null +++ b/mailcow/data/Dockerfiles/clamd/clamd.sh @@ -0,0 +1,106 @@ +#!/bin/bash + +if [[ "${SKIP_CLAMD}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + echo "SKIP_CLAMD=y, skipping ClamAV..." + sleep 365d + exit 0 +fi + +# Cleaning up garbage +echo "Cleaning up tmp files..." +rm -rf /var/lib/clamav/clamav-*.tmp + +# Prepare whitelist + +mkdir -p /run/clamav /var/lib/clamav + +if [[ -s /etc/clamav/whitelist.ign2 ]]; then + echo "Copying non-empty whitelist.ign2 to /var/lib/clamav/whitelist.ign2" + cp /etc/clamav/whitelist.ign2 /var/lib/clamav/whitelist.ign2 +fi + +if [[ ! -f /var/lib/clamav/whitelist.ign2 ]]; then + echo "Creating /var/lib/clamav/whitelist.ign2" + cat < /var/lib/clamav/whitelist.ign2 +# Please restart ClamAV after changing signatures +Example-Signature.Ignore-1 +PUA.Win.Trojan.EmbeddedPDF-1 +PUA.Pdf.Trojan.EmbeddedJavaScript-1 +PUA.Pdf.Trojan.OpenActionObjectwithJavascript-1 +EOF +fi + +chown clamav:clamav -R /var/lib/clamav /run/clamav + +chmod 755 /var/lib/clamav +chmod 644 -R /var/lib/clamav/* +chmod 750 /run/clamav + +stat /var/lib/clamav/whitelist.ign2 +dos2unix /var/lib/clamav/whitelist.ign2 +sed -i '/^\s*$/d' /var/lib/clamav/whitelist.ign2 +# Copying to /etc/clamav to expose file as-is to administrator +cp -p /var/lib/clamav/whitelist.ign2 /etc/clamav/whitelist.ign2 + + +BACKGROUND_TASKS=() + +echo "Running freshclam..." +freshclam + +( +while true; do + sleep 12600 + freshclam +done +) & +BACKGROUND_TASKS+=($!) + +( +while true; do + sleep 10m + SANE_MIRRORS="$(dig +ignore +short rsync.sanesecurity.net)" + for sane_mirror in ${SANE_MIRRORS}; do + CE= + rsync -avp --chown=clamav:clamav --chmod=Du=rwx,Dgo=rx,Fu=rw,Fog=r --timeout=5 rsync://${sane_mirror}/sanesecurity/ \ + --include 'blurl.ndb' \ + --include 'junk.ndb' \ + --include 'jurlbl.ndb' \ + --include 'jurbla.ndb' \ + --include 'phishtank.ndb' \ + --include 'phish.ndb' \ + --include 'spamimg.hdb' \ + --include 'scam.ndb' \ + --include 'rogue.hdb' \ + --include 'sanesecurity.ftm' \ + --include 'sigwhitelist.ign2' \ + --exclude='*' /var/lib/clamav/ + CE=$? + chmod 755 /var/lib/clamav/ + if [ ${CE} -eq 0 ]; then + while [ ! -z "$(pidof freshclam)" ]; do + echo "Freshclam is active, waiting..." + sleep 5 + done + echo RELOAD | nc clamd-mailcow 3310 + break + fi + done + sleep 12h +done +) & +BACKGROUND_TASKS+=($!) + +echo "$(clamd -V) is starting... please wait a moment." +nice -n10 clamd & +BACKGROUND_TASKS+=($!) + +while true; do + for bg_task in ${BACKGROUND_TASKS[*]}; do + if ! kill -0 ${bg_task} 1>&2; then + echo "Worker ${bg_task} died, stopping container waiting for respawn..." + kill -TERM 1 + fi + sleep 10 + done +done diff --git a/mailcow/data/Dockerfiles/clamd/clamdcheck.sh b/mailcow/data/Dockerfiles/clamd/clamdcheck.sh new file mode 100644 index 0000000..e7e53a6 --- /dev/null +++ b/mailcow/data/Dockerfiles/clamd/clamdcheck.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +set -eu + +if [ "${CLAMAV_NO_CLAMD:-}" != "false" ]; then + if [ "$(echo "PING" | nc localhost 3310)" != "PONG" ]; then + echo "ERROR: Unable to contact server" + exit 1 + fi + + echo "Clamd is up" +fi + +exit 0 diff --git a/mailcow/data/Dockerfiles/clamd/healthcheck.sh b/mailcow/data/Dockerfiles/clamd/healthcheck.sh new file mode 100755 index 0000000..6c18ac0 --- /dev/null +++ b/mailcow/data/Dockerfiles/clamd/healthcheck.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +if [[ "${SKIP_CLAMD}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + echo "SKIP_CLAMD=y, skipping ClamAV..." + exit 0 +fi + +# run clamd healthcheck +/usr/local/bin/clamdcheck.sh diff --git a/mailcow/data/Dockerfiles/dockerapi/Dockerfile b/mailcow/data/Dockerfiles/dockerapi/Dockerfile new file mode 100644 index 0000000..8727643 --- /dev/null +++ b/mailcow/data/Dockerfiles/dockerapi/Dockerfile @@ -0,0 +1,27 @@ +FROM alpine:3.21 + +LABEL maintainer = "The Infrastructure Company GmbH " + +ARG PIP_BREAK_SYSTEM_PACKAGES=1 +WORKDIR /app + +RUN apk add --update --no-cache python3 \ + py3-pip \ + openssl \ + tzdata \ + py3-psutil \ + py3-redis \ + py3-async-timeout \ +&& pip3 install --upgrade pip \ + fastapi \ + uvicorn \ + aiodocker \ + docker +RUN mkdir /app/modules + +COPY docker-entrypoint.sh /app/ +COPY main.py /app/main.py +COPY modules/ /app/modules/ + +ENTRYPOINT ["/bin/sh", "/app/docker-entrypoint.sh"] +CMD ["python", "main.py"] \ No newline at end of file diff --git a/mailcow/data/Dockerfiles/dockerapi/docker-entrypoint.sh b/mailcow/data/Dockerfiles/dockerapi/docker-entrypoint.sh new file mode 100755 index 0000000..64f4b82 --- /dev/null +++ b/mailcow/data/Dockerfiles/dockerapi/docker-entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +`openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \ + -keyout /app/dockerapi_key.pem \ + -out /app/dockerapi_cert.pem \ + -subj /CN=dockerapi/O=mailcow \ + -addext subjectAltName=DNS:dockerapi` + +exec "$@" diff --git a/mailcow/data/Dockerfiles/dockerapi/main.py b/mailcow/data/Dockerfiles/dockerapi/main.py new file mode 100644 index 0000000..57e2628 --- /dev/null +++ b/mailcow/data/Dockerfiles/dockerapi/main.py @@ -0,0 +1,261 @@ +import os +import sys +import uvicorn +import json +import uuid +import async_timeout +import asyncio +import aiodocker +import docker +import logging +from logging.config import dictConfig +from fastapi import FastAPI, Response, Request +from modules.DockerApi import DockerApi +from redis import asyncio as aioredis +from contextlib import asynccontextmanager + +dockerapi = None + +@asynccontextmanager +async def lifespan(app: FastAPI): + global dockerapi + + # Initialize a custom logger + logger = logging.getLogger("dockerapi") + logger.setLevel(logging.INFO) + # Configure the logger to output logs to the terminal + handler = logging.StreamHandler() + handler.setLevel(logging.INFO) + formatter = logging.Formatter("%(levelname)s: %(message)s") + handler.setFormatter(formatter) + logger.addHandler(handler) + + logger.info("Init APP") + + # Init redis client + if os.environ['REDIS_SLAVEOF_IP'] != "": + redis_client = redis = await aioredis.from_url(f"redis://{os.environ['REDIS_SLAVEOF_IP']}:{os.environ['REDIS_SLAVEOF_PORT']}/0", password=os.environ['REDISPASS']) + else: + redis_client = redis = await aioredis.from_url("redis://redis-mailcow:6379/0", password=os.environ['REDISPASS']) + + # Init docker clients + sync_docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto') + async_docker_client = aiodocker.Docker(url='unix:///var/run/docker.sock') + + dockerapi = DockerApi(redis_client, sync_docker_client, async_docker_client, logger) + + logger.info("Subscribe to redis channel") + # Subscribe to redis channel + dockerapi.pubsub = redis.pubsub() + await dockerapi.pubsub.subscribe("MC_CHANNEL") + asyncio.create_task(handle_pubsub_messages(dockerapi.pubsub)) + + + yield + + # Close docker connections + dockerapi.sync_docker_client.close() + await dockerapi.async_docker_client.close() + + # Close redis + await dockerapi.pubsub.unsubscribe("MC_CHANNEL") + await dockerapi.redis_client.close() + +app = FastAPI(lifespan=lifespan) + +# Define Routes +@app.get("/host/stats") +async def get_host_update_stats(): + global dockerapi + + if dockerapi.host_stats_isUpdating == False: + asyncio.create_task(dockerapi.get_host_stats()) + dockerapi.host_stats_isUpdating = True + + while True: + if await dockerapi.redis_client.exists('host_stats'): + break + await asyncio.sleep(1.5) + + stats = json.loads(await dockerapi.redis_client.get('host_stats')) + return Response(content=json.dumps(stats, indent=4), media_type="application/json") + +@app.get("/containers/{container_id}/json") +async def get_container(container_id : str): + global dockerapi + + if container_id and container_id.isalnum(): + try: + for container in (await dockerapi.async_docker_client.containers.list()): + if container._id == container_id: + container_info = await container.show() + return Response(content=json.dumps(container_info, indent=4), media_type="application/json") + + res = { + "type": "danger", + "msg": "no container found" + } + return Response(content=json.dumps(res, indent=4), media_type="application/json") + except Exception as e: + res = { + "type": "danger", + "msg": str(e) + } + return Response(content=json.dumps(res, indent=4), media_type="application/json") + else: + res = { + "type": "danger", + "msg": "no or invalid id defined" + } + return Response(content=json.dumps(res, indent=4), media_type="application/json") + +@app.get("/containers/json") +async def get_containers(): + global dockerapi + + containers = {} + try: + for container in (await dockerapi.async_docker_client.containers.list()): + container_info = await container.show() + containers.update({container_info['Id']: container_info}) + return Response(content=json.dumps(containers, indent=4), media_type="application/json") + except Exception as e: + res = { + "type": "danger", + "msg": str(e) + } + return Response(content=json.dumps(res, indent=4), media_type="application/json") + +@app.post("/containers/{container_id}/{post_action}") +async def post_containers(container_id : str, post_action : str, request: Request): + global dockerapi + + try: + request_json = await request.json() + except Exception as err: + request_json = {} + + if container_id and container_id.isalnum() and post_action: + try: + """Dispatch container_post api call""" + if post_action == 'exec': + if not request_json or not 'cmd' in request_json: + res = { + "type": "danger", + "msg": "cmd is missing" + } + return Response(content=json.dumps(res, indent=4), media_type="application/json") + if not request_json or not 'task' in request_json: + res = { + "type": "danger", + "msg": "task is missing" + } + return Response(content=json.dumps(res, indent=4), media_type="application/json") + + api_call_method_name = '__'.join(['container_post', str(post_action), str(request_json['cmd']), str(request_json['task']) ]) + else: + api_call_method_name = '__'.join(['container_post', str(post_action) ]) + + api_call_method = getattr(dockerapi, api_call_method_name, lambda container_id: Response(content=json.dumps({'type': 'danger', 'msg':'container_post - unknown api call' }, indent=4), media_type="application/json")) + + dockerapi.logger.info("api call: %s, container_id: %s" % (api_call_method_name, container_id)) + return api_call_method(request_json, container_id=container_id) + except Exception as e: + dockerapi.logger.error("error - container_post: %s" % str(e)) + res = { + "type": "danger", + "msg": str(e) + } + return Response(content=json.dumps(res, indent=4), media_type="application/json") + + else: + res = { + "type": "danger", + "msg": "invalid container id or missing action" + } + return Response(content=json.dumps(res, indent=4), media_type="application/json") + +@app.post("/container/{container_id}/stats/update") +async def post_container_update_stats(container_id : str): + global dockerapi + + # start update task for container if no task is running + if container_id not in dockerapi.containerIds_to_update: + asyncio.create_task(dockerapi.get_container_stats(container_id)) + dockerapi.containerIds_to_update.append(container_id) + + while True: + if await dockerapi.redis_client.exists(container_id + '_stats'): + break + await asyncio.sleep(1.5) + + stats = json.loads(await dockerapi.redis_client.get(container_id + '_stats')) + return Response(content=json.dumps(stats, indent=4), media_type="application/json") + + +# PubSub Handler +async def handle_pubsub_messages(channel: aioredis.client.PubSub): + global dockerapi + + while True: + try: + async with async_timeout.timeout(60): + message = await channel.get_message(ignore_subscribe_messages=True, timeout=30) + if message is not None: + # Parse message + data_json = json.loads(message['data'].decode('utf-8')) + dockerapi.logger.info(f"PubSub Received - {json.dumps(data_json)}") + + # Handle api_call + if 'api_call' in data_json: + # api_call: container_post + if data_json['api_call'] == "container_post": + if 'post_action' in data_json and 'container_name' in data_json: + try: + """Dispatch container_post api call""" + request_json = {} + if data_json['post_action'] == 'exec': + if 'request' in data_json: + request_json = data_json['request'] + if 'cmd' in request_json: + if 'task' in request_json: + api_call_method_name = '__'.join(['container_post', str(data_json['post_action']), str(request_json['cmd']), str(request_json['task']) ]) + else: + dockerapi.logger.error("api call: task missing") + else: + dockerapi.logger.error("api call: cmd missing") + else: + dockerapi.logger.error("api call: request missing") + else: + api_call_method_name = '__'.join(['container_post', str(data_json['post_action'])]) + + if api_call_method_name: + api_call_method = getattr(dockerapi, api_call_method_name) + if api_call_method: + dockerapi.logger.info("api call: %s, container_name: %s" % (api_call_method_name, data_json['container_name'])) + api_call_method(request_json, container_name=data_json['container_name']) + else: + dockerapi.logger.error("api call not found: %s, container_name: %s" % (api_call_method_name, data_json['container_name'])) + except Exception as e: + dockerapi.logger.error("container_post: %s" % str(e)) + else: + dockerapi.logger.error("api call: missing container_name, post_action or request") + else: + dockerapi.logger.error("Unknown PubSub received - %s" % json.dumps(data_json)) + else: + dockerapi.logger.error("Unknown PubSub received - %s" % json.dumps(data_json)) + + await asyncio.sleep(0.0) + except asyncio.TimeoutError: + pass + +if __name__ == '__main__': + uvicorn.run( + app, + host="0.0.0.0", + port=443, + ssl_certfile="/app/dockerapi_cert.pem", + ssl_keyfile="/app/dockerapi_key.pem", + log_level="info", + loop="none" + ) diff --git a/mailcow/data/Dockerfiles/dockerapi/modules/DockerApi.py b/mailcow/data/Dockerfiles/dockerapi/modules/DockerApi.py new file mode 100644 index 0000000..4701cbf --- /dev/null +++ b/mailcow/data/Dockerfiles/dockerapi/modules/DockerApi.py @@ -0,0 +1,626 @@ +import psutil +import sys +import os +import re +import time +import json +import asyncio +import platform +from datetime import datetime +from fastapi import FastAPI, Response, Request + +class DockerApi: + def __init__(self, redis_client, sync_docker_client, async_docker_client, logger): + self.redis_client = redis_client + self.sync_docker_client = sync_docker_client + self.async_docker_client = async_docker_client + self.logger = logger + + self.host_stats_isUpdating = False + self.containerIds_to_update = [] + + # api call: container_post - post_action: stop + def container_post__stop(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + for container in self.sync_docker_client.containers.list(all=True, filters=filters): + container.stop() + + res = { 'type': 'success', 'msg': 'command completed successfully'} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + # api call: container_post - post_action: start + def container_post__start(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + for container in self.sync_docker_client.containers.list(all=True, filters=filters): + container.start() + + res = { 'type': 'success', 'msg': 'command completed successfully'} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + # api call: container_post - post_action: restart + def container_post__restart(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + for container in self.sync_docker_client.containers.list(all=True, filters=filters): + container.restart() + + res = { 'type': 'success', 'msg': 'command completed successfully'} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + # api call: container_post - post_action: top + def container_post__top(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + for container in self.sync_docker_client.containers.list(all=True, filters=filters): + res = { 'type': 'success', 'msg': container.top()} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + # api call: container_post - post_action: stats + def container_post__stats(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + for container in self.sync_docker_client.containers.list(all=True, filters=filters): + for stat in container.stats(decode=True, stream=True): + res = { 'type': 'success', 'msg': stat} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + # api call: container_post - post_action: exec - cmd: mailq - task: delete + def container_post__exec__mailq__delete(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + if 'items' in request_json: + r = re.compile("^[0-9a-fA-F]+$") + filtered_qids = filter(r.match, request_json['items']) + if filtered_qids: + flagged_qids = ['-d %s' % i for i in filtered_qids] + sanitized_string = str(' '.join(flagged_qids)) + for container in self.sync_docker_client.containers.list(filters=filters): + postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string]) + return self.exec_run_handler('generic', postsuper_r) + # api call: container_post - post_action: exec - cmd: mailq - task: hold + def container_post__exec__mailq__hold(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + if 'items' in request_json: + r = re.compile("^[0-9a-fA-F]+$") + filtered_qids = filter(r.match, request_json['items']) + if filtered_qids: + flagged_qids = ['-h %s' % i for i in filtered_qids] + sanitized_string = str(' '.join(flagged_qids)) + for container in self.sync_docker_client.containers.list(filters=filters): + postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string]) + return self.exec_run_handler('generic', postsuper_r) + # api call: container_post - post_action: exec - cmd: mailq - task: cat + def container_post__exec__mailq__cat(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + if 'items' in request_json: + r = re.compile("^[0-9a-fA-F]+$") + filtered_qids = filter(r.match, request_json['items']) + if filtered_qids: + sanitized_string = str(' '.join(filtered_qids)) + + for container in self.sync_docker_client.containers.list(filters=filters): + postcat_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postcat -q " + sanitized_string], user='postfix') + if not postcat_return: + postcat_return = 'err: invalid' + return self.exec_run_handler('utf8_text_only', postcat_return) + # api call: container_post - post_action: exec - cmd: mailq - task: unhold + def container_post__exec__mailq__unhold(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + if 'items' in request_json: + r = re.compile("^[0-9a-fA-F]+$") + filtered_qids = filter(r.match, request_json['items']) + if filtered_qids: + flagged_qids = ['-H %s' % i for i in filtered_qids] + sanitized_string = str(' '.join(flagged_qids)) + for container in self.sync_docker_client.containers.list(filters=filters): + postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string]) + return self.exec_run_handler('generic', postsuper_r) + # api call: container_post - post_action: exec - cmd: mailq - task: deliver + def container_post__exec__mailq__deliver(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + if 'items' in request_json: + r = re.compile("^[0-9a-fA-F]+$") + filtered_qids = filter(r.match, request_json['items']) + if filtered_qids: + flagged_qids = ['-i %s' % i for i in filtered_qids] + for container in self.sync_docker_client.containers.list(filters=filters): + for i in flagged_qids: + postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix') + # todo: check each exit code + res = { 'type': 'success', 'msg': 'Scheduled immediate delivery'} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + # api call: container_post - post_action: exec - cmd: mailq - task: list + def container_post__exec__mailq__list(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + for container in self.sync_docker_client.containers.list(filters=filters): + mailq_return = container.exec_run(["/usr/sbin/postqueue", "-j"], user='postfix') + return self.exec_run_handler('utf8_text_only', mailq_return) + # api call: container_post - post_action: exec - cmd: mailq - task: flush + def container_post__exec__mailq__flush(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + for container in self.sync_docker_client.containers.list(filters=filters): + postqueue_r = container.exec_run(["/usr/sbin/postqueue", "-f"], user='postfix') + return self.exec_run_handler('generic', postqueue_r) + # api call: container_post - post_action: exec - cmd: mailq - task: super_delete + def container_post__exec__mailq__super_delete(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + for container in self.sync_docker_client.containers.list(filters=filters): + postsuper_r = container.exec_run(["/usr/sbin/postsuper", "-d", "ALL"]) + return self.exec_run_handler('generic', postsuper_r) + # api call: container_post - post_action: exec - cmd: system - task: fts_rescan + def container_post__exec__system__fts_rescan(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + if 'username' in request_json: + for container in self.sync_docker_client.containers.list(filters=filters): + rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -u '" + request_json['username'].replace("'", "'\\''") + "'"], user='vmail') + if rescan_return.exit_code == 0: + res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + else: + res = { 'type': 'warning', 'msg': 'fts_rescan error'} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + if 'all' in request_json: + for container in self.sync_docker_client.containers.list(filters=filters): + rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -A"], user='vmail') + if rescan_return.exit_code == 0: + res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + else: + res = { 'type': 'warning', 'msg': 'fts_rescan error'} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + # api call: container_post - post_action: exec - cmd: system - task: df + def container_post__exec__system__df(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + if 'dir' in request_json: + for container in self.sync_docker_client.containers.list(filters=filters): + df_return = container.exec_run(["/bin/bash", "-c", "/bin/df -H '" + request_json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody') + if df_return.exit_code == 0: + return df_return.output.decode('utf-8').rstrip() + else: + return "0,0,0,0,0,0" + # api call: container_post - post_action: exec - cmd: system - task: mysql_upgrade + def container_post__exec__system__mysql_upgrade(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + for container in self.sync_docker_client.containers.list(filters=filters): + sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n"], user='mysql') + if sql_return.exit_code == 0: + matched = False + for line in sql_return.output.decode('utf-8').split("\n"): + if 'is already upgraded to' in line: + matched = True + if matched: + res = { 'type': 'success', 'msg':'mysql_upgrade: already upgraded', 'text': sql_return.output.decode('utf-8')} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + else: + container.restart() + res = { 'type': 'warning', 'msg':'mysql_upgrade: upgrade was applied', 'text': sql_return.output.decode('utf-8')} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + else: + res = { 'type': 'error', 'msg': 'mysql_upgrade: error running command', 'text': sql_return.output.decode('utf-8')} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + # api call: container_post - post_action: exec - cmd: system - task: mysql_tzinfo_to_sql + def container_post__exec__system__mysql_tzinfo_to_sql(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + for container in self.sync_docker_client.containers.list(filters=filters): + sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_tzinfo_to_sql /usr/share/zoneinfo | /bin/sed 's/Local time zone must be set--see zic manual page/FCTY/' | /usr/bin/mysql -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "' mysql \n"], user='mysql') + if sql_return.exit_code == 0: + res = { 'type': 'info', 'msg': 'mysql_tzinfo_to_sql: command completed successfully', 'text': sql_return.output.decode('utf-8')} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + else: + res = { 'type': 'error', 'msg': 'mysql_tzinfo_to_sql: error running command', 'text': sql_return.output.decode('utf-8')} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + # api call: container_post - post_action: exec - cmd: reload - task: dovecot + def container_post__exec__reload__dovecot(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + for container in self.sync_docker_client.containers.list(filters=filters): + reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/dovecot reload"]) + return self.exec_run_handler('generic', reload_return) + # api call: container_post - post_action: exec - cmd: reload - task: postfix + def container_post__exec__reload__postfix(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + for container in self.sync_docker_client.containers.list(filters=filters): + reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postfix reload"]) + return self.exec_run_handler('generic', reload_return) + # api call: container_post - post_action: exec - cmd: reload - task: nginx + def container_post__exec__reload__nginx(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + for container in self.sync_docker_client.containers.list(filters=filters): + reload_return = container.exec_run(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"]) + return self.exec_run_handler('generic', reload_return) + # api call: container_post - post_action: exec - cmd: sieve - task: list + def container_post__exec__sieve__list(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + if 'username' in request_json: + for container in self.sync_docker_client.containers.list(filters=filters): + sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm sieve list -u '" + request_json['username'].replace("'", "'\\''") + "'"]) + return self.exec_run_handler('utf8_text_only', sieve_return) + # api call: container_post - post_action: exec - cmd: sieve - task: print + def container_post__exec__sieve__print(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + if 'username' in request_json and 'script_name' in request_json: + for container in self.sync_docker_client.containers.list(filters=filters): + cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' '" + request_json['script_name'].replace("'", "'\\''") + "'"] + sieve_return = container.exec_run(cmd) + return self.exec_run_handler('utf8_text_only', sieve_return) + # api call: container_post - post_action: exec - cmd: maildir - task: cleanup + def container_post__exec__maildir__cleanup(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + if 'maildir' in request_json: + for container in self.sync_docker_client.containers.list(filters=filters): + sane_name = re.sub(r'\W+', '', request_json['maildir']) + vmail_name = request_json['maildir'].replace("'", "'\\''") + cmd_vmail = "if [[ -d '/var/vmail/" + vmail_name + "' ]]; then /bin/mv '/var/vmail/" + vmail_name + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi" + index_name = request_json['maildir'].split("/") + if len(index_name) > 1: + index_name = index_name[1].replace("'", "'\\''") + "@" + index_name[0].replace("'", "'\\''") + cmd_vmail_index = "if [[ -d '/var/vmail_index/" + index_name + "' ]]; then /bin/mv '/var/vmail_index/" + index_name + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "_index'; fi" + cmd = ["/bin/bash", "-c", cmd_vmail + " && " + cmd_vmail_index] + else: + cmd = ["/bin/bash", "-c", cmd_vmail] + maildir_cleanup = container.exec_run(cmd, user='vmail') + return self.exec_run_handler('generic', maildir_cleanup) + # api call: container_post - post_action: exec - cmd: maildir - task: move + def container_post__exec__maildir__move(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + if 'old_maildir' in request_json and 'new_maildir' in request_json: + for container in self.sync_docker_client.containers.list(filters=filters): + vmail_name = request_json['old_maildir'].replace("'", "'\\''") + new_vmail_name = request_json['new_maildir'].replace("'", "'\\''") + cmd_vmail = f"if [[ -d '/var/vmail/{vmail_name}' ]]; then /bin/mv '/var/vmail/{vmail_name}' '/var/vmail/{new_vmail_name}'; fi" + + index_name = request_json['old_maildir'].split("/") + new_index_name = request_json['new_maildir'].split("/") + if len(index_name) > 1 and len(new_index_name) > 1: + index_name = index_name[1].replace("'", "'\\''") + "@" + index_name[0].replace("'", "'\\''") + new_index_name = new_index_name[1].replace("'", "'\\''") + "@" + new_index_name[0].replace("'", "'\\''") + cmd_vmail_index = f"if [[ -d '/var/vmail_index/{index_name}' ]]; then /bin/mv '/var/vmail_index/{index_name}' '/var/vmail_index/{new_index_name}_index'; fi" + cmd = ["/bin/bash", "-c", cmd_vmail + " && " + cmd_vmail_index] + else: + cmd = ["/bin/bash", "-c", cmd_vmail] + maildir_move = container.exec_run(cmd, user='vmail') + return self.exec_run_handler('generic', maildir_move) + # api call: container_post - post_action: exec - cmd: rspamd - task: worker_password + def container_post__exec__rspamd__worker_password(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + if 'raw' in request_json: + for container in self.sync_docker_client.containers.list(filters=filters): + cmd = "/usr/bin/rspamadm pw -e -p '" + request_json['raw'].replace("'", "'\\''") + "' 2> /dev/null" + cmd_response = self.exec_cmd_container(container, cmd, user="_rspamd") + + matched = False + for line in cmd_response.split("\n"): + if '$2$' in line: + hash = line.strip() + hash_out = re.search(r'\$2\$.+$', hash).group(0) + rspamd_passphrase_hash = re.sub(r'[^0-9a-zA-Z\$]+', '', hash_out.rstrip()) + rspamd_password_filename = "/etc/rspamd/override.d/worker-controller-password.inc" + cmd = '''/bin/echo 'enable_password = "%s";' > %s && cat %s''' % (rspamd_passphrase_hash, rspamd_password_filename, rspamd_password_filename) + cmd_response = self.exec_cmd_container(container, cmd, user="_rspamd") + if rspamd_passphrase_hash.startswith("$2$") and rspamd_passphrase_hash in cmd_response: + container.restart() + matched = True + if matched: + res = { 'type': 'success', 'msg': 'command completed successfully' } + self.logger.info('success changing Rspamd password') + return Response(content=json.dumps(res, indent=4), media_type="application/json") + else: + self.logger.error('failed changing Rspamd password') + res = { 'type': 'danger', 'msg': 'command did not complete' } + return Response(content=json.dumps(res, indent=4), media_type="application/json") + # api call: container_post - post_action: exec - cmd: sogo - task: rename + def container_post__exec__sogo__rename_user(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + if 'old_username' in request_json and 'new_username' in request_json: + for container in self.sync_docker_client.containers.list(filters=filters): + old_username = request_json['old_username'].replace("'", "'\\''") + new_username = request_json['new_username'].replace("'", "'\\''") + + sogo_return = container.exec_run(["/bin/bash", "-c", f"sogo-tool rename-user '{old_username}' '{new_username}'"], user='sogo') + return self.exec_run_handler('generic', sogo_return) + # api call: container_post - post_action: exec - cmd: doveadm - task: get_acl + def container_post__exec__doveadm__get_acl(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + for container in self.sync_docker_client.containers.list(filters=filters): + id = request_json['id'].replace("'", "'\\''") + + shared_folders = container.exec_run(["/bin/bash", "-c", f"doveadm mailbox list -u '{id}'"]) + shared_folders = shared_folders.output.decode('utf-8') + shared_folders = shared_folders.splitlines() + + formatted_acls = [] + mailbox_seen = [] + for shared_folder in shared_folders: + if "Shared" not in shared_folder: + mailbox = shared_folder.replace("'", "'\\''") + if mailbox in mailbox_seen: + continue + + acls = container.exec_run(["/bin/bash", "-c", f"doveadm acl get -u '{id}' '{mailbox}'"]) + acls = acls.output.decode('utf-8').strip().splitlines() + if len(acls) >= 2: + for acl in acls[1:]: + user_id, rights = acl.split(maxsplit=1) + user_id = user_id.split('=')[1] + mailbox_seen.append(mailbox) + formatted_acls.append({ 'user': id, 'id': user_id, 'mailbox': mailbox, 'rights': rights.split() }) + elif "Shared" in shared_folder and "/" in shared_folder: + shared_folder = shared_folder.split("/") + if len(shared_folder) < 3: + continue + + user = shared_folder[1].replace("'", "'\\''") + mailbox = '/'.join(shared_folder[2:]).replace("'", "'\\''") + if mailbox in mailbox_seen: + continue + + acls = container.exec_run(["/bin/bash", "-c", f"doveadm acl get -u '{user}' '{mailbox}'"]) + acls = acls.output.decode('utf-8').strip().splitlines() + if len(acls) >= 2: + for acl in acls[1:]: + user_id, rights = acl.split(maxsplit=1) + user_id = user_id.split('=')[1].replace("'", "'\\''") + if user_id == id and mailbox not in mailbox_seen: + mailbox_seen.append(mailbox) + formatted_acls.append({ 'user': user, 'id': id, 'mailbox': mailbox, 'rights': rights.split() }) + + return Response(content=json.dumps(formatted_acls, indent=4), media_type="application/json") + # api call: container_post - post_action: exec - cmd: doveadm - task: delete_acl + def container_post__exec__doveadm__delete_acl(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + for container in self.sync_docker_client.containers.list(filters=filters): + user = request_json['user'].replace("'", "'\\''") + mailbox = request_json['mailbox'].replace("'", "'\\''") + id = request_json['id'].replace("'", "'\\''") + + if user and mailbox and id: + acl_delete_return = container.exec_run(["/bin/bash", "-c", f"doveadm acl delete -u '{user}' '{mailbox}' 'user={id}'"]) + return self.exec_run_handler('generic', acl_delete_return) + # api call: container_post - post_action: exec - cmd: doveadm - task: set_acl + def container_post__exec__doveadm__set_acl(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + for container in self.sync_docker_client.containers.list(filters=filters): + user = request_json['user'].replace("'", "'\\''") + mailbox = request_json['mailbox'].replace("'", "'\\''") + id = request_json['id'].replace("'", "'\\''") + rights = "" + + available_rights = [ + "admin", + "create", + "delete", + "expunge", + "insert", + "lookup", + "post", + "read", + "write", + "write-deleted", + "write-seen" + ] + for right in request_json['rights']: + right = right.replace("'", "'\\''").lower() + if right in available_rights: + rights += right + " " + + if user and mailbox and id and rights: + acl_set_return = container.exec_run(["/bin/bash", "-c", f"doveadm acl set -u '{user}' '{mailbox}' 'user={id}' {rights}"]) + return self.exec_run_handler('generic', acl_set_return) + + + # Collect host stats + async def get_host_stats(self, wait=5): + try: + system_time = datetime.now() + host_stats = { + "cpu": { + "cores": psutil.cpu_count(), + "usage": psutil.cpu_percent() + }, + "memory": { + "total": psutil.virtual_memory().total, + "usage": psutil.virtual_memory().percent, + "swap": psutil.swap_memory() + }, + "uptime": time.time() - psutil.boot_time(), + "system_time": system_time.strftime("%d.%m.%Y %H:%M:%S"), + "architecture": platform.machine() + } + + await self.redis_client.set('host_stats', json.dumps(host_stats), ex=10) + except Exception as e: + res = { + "type": "danger", + "msg": str(e) + } + + await asyncio.sleep(wait) + self.host_stats_isUpdating = False + # Collect container stats + async def get_container_stats(self, container_id, wait=5, stop=False): + if container_id and container_id.isalnum(): + try: + for container in (await self.async_docker_client.containers.list()): + if container._id == container_id: + res = await container.stats(stream=False) + + if await self.redis_client.exists(container_id + '_stats'): + stats = json.loads(await self.redis_client.get(container_id + '_stats')) + else: + stats = [] + stats.append(res[0]) + if len(stats) > 3: + del stats[0] + await self.redis_client.set(container_id + '_stats', json.dumps(stats), ex=60) + except Exception as e: + res = { + "type": "danger", + "msg": str(e) + } + else: + res = { + "type": "danger", + "msg": "no or invalid id defined" + } + + await asyncio.sleep(wait) + if stop == True: + # update task was called second time, stop + self.containerIds_to_update.remove(container_id) + else: + # call update task a second time + await self.get_container_stats(container_id, wait=0, stop=True) + + def exec_cmd_container(self, container, cmd, user, timeout=2, shell_cmd="/bin/bash"): + def recv_socket_data(c_socket, timeout): + c_socket.setblocking(0) + total_data=[] + data='' + begin=time.time() + while True: + if total_data and time.time()-begin > timeout: + break + elif time.time()-begin > timeout*2: + break + try: + data = c_socket.recv(8192) + if data: + total_data.append(data.decode('utf-8')) + #change the beginning time for measurement + begin=time.time() + else: + #sleep for sometime to indicate a gap + time.sleep(0.1) + break + except: + pass + return ''.join(total_data) + + try : + socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock + if not cmd.endswith("\n"): + cmd = cmd + "\n" + socket.send(cmd.encode('utf-8')) + data = recv_socket_data(socket, timeout) + socket.close() + return data + except Exception as e: + self.logger.error("error - exec_cmd_container: %s" % str(e)) + traceback.print_exc(file=sys.stdout) + + def exec_run_handler(self, type, output): + if type == 'generic': + if output.exit_code == 0: + res = { 'type': 'success', 'msg': 'command completed successfully' } + return Response(content=json.dumps(res, indent=4), media_type="application/json") + else: + res = { 'type': 'danger', 'msg': 'command failed: ' + output.output.decode('utf-8') } + return Response(content=json.dumps(res, indent=4), media_type="application/json") + if type == 'utf8_text_only': + return Response(content=output.output.decode('utf-8'), media_type="text/plain") diff --git a/mailcow/data/Dockerfiles/dockerapi/modules/__init__.py b/mailcow/data/Dockerfiles/dockerapi/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mailcow/data/Dockerfiles/dovecot/Dockerfile b/mailcow/data/Dockerfiles/dovecot/Dockerfile new file mode 100644 index 0000000..9e49d88 --- /dev/null +++ b/mailcow/data/Dockerfiles/dovecot/Dockerfile @@ -0,0 +1,139 @@ +FROM alpine:3.21 + +LABEL maintainer="The Infrastructure Company GmbH " + +# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?.*)$ +ARG GOSU_VERSION=1.16 + +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 + +# Add groups and users before installing Dovecot to not break compatibility +RUN addgroup -g 5000 vmail \ + && addgroup -g 401 dovecot \ + && addgroup -g 402 dovenull \ + && sed -i "s/999/99/" /etc/group \ + && addgroup -g 999 sogo \ + && addgroup nobody sogo \ + && adduser -D -u 5000 -G vmail -h /var/vmail vmail \ + && adduser -D -G dovecot -u 401 -h /dev/null -s /sbin/nologin dovecot \ + && adduser -D -G dovenull -u 402 -h /dev/null -s /sbin/nologin dovenull \ + && apk add --no-cache --update \ + bash \ + bind-tools \ + findutils \ + envsubst \ + ca-certificates \ + curl \ + coreutils \ + jq \ + lua \ + lua-cjson \ + lua-socket \ + lua-sql-mysql \ + lua5.3-sql-mysql \ + icu-data-full \ + mariadb-connector-c \ + lua-sec \ + mariadb-dev \ + glib-dev \ + gcompat \ + mariadb-client \ + perl \ + perl-dev \ + perl-ntlm \ + perl-cgi \ + perl-crypt-openssl-rsa \ + perl-utils \ + perl-crypt-ssleay \ + perl-data-uniqid \ + perl-dbd-mysql \ + perl-dbi \ + perl-digest-hmac \ + perl-dist-checkconflicts \ + perl-encode-imaputf7 \ + perl-file-copy-recursive \ + perl-file-tail \ + perl-io-socket-inet6 \ + perl-io-gzip \ + perl-io-socket-ssl \ + perl-io-tee \ + perl-ipc-run \ + perl-json-webtoken \ + perl-mail-imapclient \ + perl-module-implementation \ + perl-module-scandeps \ + perl-net-ssleay \ + perl-package-stash \ + perl-package-stash-xs \ + perl-par-packer \ + perl-parse-recdescent \ + perl-lockfile-simple \ + libproc2 \ + perl-readonly \ + perl-regexp-common \ + perl-sys-meminfo \ + perl-term-readkey \ + perl-test-deep \ + perl-test-fatal \ + perl-test-mockobject \ + perl-test-mock-guard \ + perl-test-pod \ + perl-test-requires \ + perl-test-simple \ + perl-test-warn \ + perl-try-tiny \ + perl-unicode-string \ + perl-proc-processtable \ + perl-app-cpanminus \ + procps \ + python3 \ + py3-mysqlclient \ + py3-html2text \ + py3-jinja2 \ + py3-redis \ + redis \ + syslog-ng \ + syslog-ng-redis \ + syslog-ng-json \ + supervisor \ + tzdata \ + wget \ + dovecot \ + dovecot-dev \ + dovecot-lmtpd \ + dovecot-lua \ + dovecot-ldap \ + dovecot-mysql \ + dovecot-sql \ + dovecot-submissiond \ + dovecot-pigeonhole-plugin \ + dovecot-pop3d \ + dovecot-fts-flatcurve \ + && arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \ + && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$arch" \ + && chmod +x /usr/local/bin/gosu \ + && gosu nobody true + +COPY trim_logs.sh /usr/local/bin/trim_logs.sh +COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh +COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf +COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf +COPY imapsync /usr/local/bin/imapsync +COPY imapsync_runner.pl /usr/local/bin/imapsync_runner.pl +COPY report-spam.sieve /usr/lib/dovecot/sieve/report-spam.sieve +COPY report-ham.sieve /usr/lib/dovecot/sieve/report-ham.sieve +COPY rspamd-pipe-ham /usr/lib/dovecot/sieve/rspamd-pipe-ham +COPY rspamd-pipe-spam /usr/lib/dovecot/sieve/rspamd-pipe-spam +COPY sa-rules.sh /usr/local/bin/sa-rules.sh +COPY maildir_gc.sh /usr/local/bin/maildir_gc.sh +COPY docker-entrypoint.sh / +COPY supervisord.conf /etc/supervisor/supervisord.conf +COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh +COPY quarantine_notify.py /usr/local/bin/quarantine_notify.py +COPY quota_notify.py /usr/local/bin/quota_notify.py +COPY repl_health.sh /usr/local/bin/repl_health.sh +COPY optimize-fts.sh /usr/local/bin/optimize-fts.sh + +ENTRYPOINT ["/docker-entrypoint.sh"] +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] diff --git a/mailcow/data/Dockerfiles/dovecot/clean_q_aged.sh b/mailcow/data/Dockerfiles/dovecot/clean_q_aged.sh new file mode 100755 index 0000000..ebedc89 --- /dev/null +++ b/mailcow/data/Dockerfiles/dovecot/clean_q_aged.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +source /source_env.sh + +MAX_AGE=$(redis-cli --raw -h redis-mailcow -a ${REDISPASS} --no-auth-warning GET Q_MAX_AGE) + +if [[ -z ${MAX_AGE} ]]; then + echo "Max age for quarantine items not defined" + exit 1 +fi + +NUM_REGEXP='^[0-9]+$' +if ! [[ ${MAX_AGE} =~ ${NUM_REGEXP} ]] ; then + echo "Max age for quarantine items invalid" + exit 1 +fi + +TO_DELETE=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT COUNT(id) FROM quarantine WHERE created < NOW() - INTERVAL ${MAX_AGE//[!0-9]/} DAY" -BN) +mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DELETE FROM quarantine WHERE created < NOW() - INTERVAL ${MAX_AGE//[!0-9]/} DAY" +echo "Deleted ${TO_DELETE} items from quarantine table (max age is ${MAX_AGE//[!0-9]/} days)" diff --git a/mailcow/data/Dockerfiles/dovecot/docker-entrypoint.sh b/mailcow/data/Dockerfiles/dovecot/docker-entrypoint.sh new file mode 100755 index 0000000..fe2341b --- /dev/null +++ b/mailcow/data/Dockerfiles/dovecot/docker-entrypoint.sh @@ -0,0 +1,348 @@ +#!/bin/bash +set -e + +# Wait for MySQL to warm-up +while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do + echo "Waiting for database to come up..." + sleep 2 +done + +until dig +short mailcow.email > /dev/null; do + echo "Waiting for DNS..." + sleep 1 +done + +# Do not attempt to write to slave +if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then + REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning" +else + REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning" +fi + +until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do + echo "Waiting for Redis..." + sleep 2 +done + +${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null + +# Create missing directories +[[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/ +[[ ! -d /etc/dovecot/auth/ ]] && mkdir -p /etc/dovecot/auth/ +[[ ! -d /etc/dovecot/conf.d/ ]] && mkdir -p /etc/dovecot/conf.d/ +[[ ! -d /var/vmail/_garbage ]] && mkdir -p /var/vmail/_garbage +[[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve +[[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo +[[ ! -d /var/volatile ]] && mkdir -p /var/volatile + +# Set Dovecot sql config parameters, escape " in db password +DBPASS=$(echo ${DBPASS} | sed 's/"/\\"/g') + +# Create quota dict for Dovecot +if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + QUOTA_TABLE=quota2 +else + QUOTA_TABLE=quota2replica +fi +cat < /etc/dovecot/sql/dovecot-dict-sql-quota.conf +# Autogenerated by mailcow +connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}" +map { + pattern = priv/quota/storage + table = ${QUOTA_TABLE} + username_field = username + value_field = bytes +} +map { + pattern = priv/quota/messages + table = ${QUOTA_TABLE} + username_field = username + value_field = messages +} +EOF + +# Create dict used for sieve pre and postfilters +cat < /etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf +# Autogenerated by mailcow +connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}" +map { + pattern = priv/sieve/name/\$script_name + table = sieve_before + username_field = username + value_field = id + fields { + script_name = \$script_name + } +} +map { + pattern = priv/sieve/data/\$id + table = sieve_before + username_field = username + value_field = script_data + fields { + id = \$id + } +} +EOF + +cat < /etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf +# Autogenerated by mailcow +connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}" +map { + pattern = priv/sieve/name/\$script_name + table = sieve_after + username_field = username + value_field = id + fields { + script_name = \$script_name + } +} +map { + pattern = priv/sieve/data/\$id + table = sieve_after + username_field = username + value_field = script_data + fields { + id = \$id + } +} +EOF + +echo -n ${ACL_ANYONE} > /etc/dovecot/acl_anyone + +if [[ "${SKIP_FTS}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then +echo -e "\e[33mDetecting SKIP_FTS=y... not enabling Flatcurve (FTS) then...\e[0m" +echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify listescape replication lazy_expunge' > /etc/dovecot/mail_plugins +echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify listescape replication mail_log' > /etc/dovecot/mail_plugins_imap +echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl notify listescape replication' > /etc/dovecot/mail_plugins_lmtp +else +echo -e "\e[32mDetecting SKIP_FTS=n... enabling Flatcurve (FTS)\e[0m" +echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify fts fts_flatcurve listescape replication lazy_expunge' > /etc/dovecot/mail_plugins +echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify mail_log fts fts_flatcurve listescape replication' > /etc/dovecot/mail_plugins_imap +echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl fts fts_flatcurve notify listescape replication' > /etc/dovecot/mail_plugins_lmtp +fi +chmod 644 /etc/dovecot/mail_plugins /etc/dovecot/mail_plugins_imap /etc/dovecot/mail_plugins_lmtp /templates/quarantine.tpl + +cat < /etc/dovecot/sql/dovecot-dict-sql-userdb.conf +# Autogenerated by mailcow +driver = mysql +connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}" +user_query = SELECT CONCAT(JSON_UNQUOTE(JSON_VALUE(attributes, '$.mailbox_format')), mailbox_path_prefix, '%d/%n/${MAILDIR_SUB}:VOLATILEDIR=/var/volatile/%u:INDEX=/var/vmail_index/%u') AS mail, '%s' AS protocol, 5000 AS uid, 5000 AS gid, concat('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%u' AND (active = '1' OR active = '2') +iterate_query = SELECT username FROM mailbox WHERE active = '1' OR active = '2'; +EOF + + +# Migrate old sieve_after file +[[ -f /etc/dovecot/sieve_after ]] && mv /etc/dovecot/sieve_after /etc/dovecot/global_sieve_after +# Create global sieve scripts +cat /etc/dovecot/global_sieve_after > /var/vmail/sieve/global_sieve_after.sieve +cat /etc/dovecot/global_sieve_before > /var/vmail/sieve/global_sieve_before.sieve + +# Check permissions of vmail/index/garbage directories. +# Do not do this every start-up, it may take a very long time. So we use a stat check here. +if [[ $(stat -c %U /var/vmail/) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail ; fi +if [[ $(stat -c %U /var/vmail/_garbage) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail/_garbage ; fi +if [[ $(stat -c %U /var/vmail_index) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail_index ; fi + +# Cleanup random user maildirs +rm -rf /var/vmail/mailcow.local/* +# Cleanup PIDs +[[ -f /tmp/quarantine_notify.pid ]] && rm /tmp/quarantine_notify.pid + +# create sni configuration +echo "" > /etc/dovecot/sni.conf +for cert_dir in /etc/ssl/mail/*/ ; do + if [[ ! -f ${cert_dir}domains ]] || [[ ! -f ${cert_dir}cert.pem ]] || [[ ! -f ${cert_dir}key.pem ]]; then + continue + fi + domains=($(cat ${cert_dir}domains)) + for domain in ${domains[@]}; do + echo 'local_name '${domain}' {' >> /etc/dovecot/sni.conf; + echo ' ssl_cert = <'${cert_dir}'cert.pem' >> /etc/dovecot/sni.conf; + echo ' ssl_key = <'${cert_dir}'key.pem' >> /etc/dovecot/sni.conf; + echo '}' >> /etc/dovecot/sni.conf; + done +done + +# Create random master for SOGo sieve features +RAND_USER=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 16 | head -n 1) +RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 24 | head -n 1) + +if [[ ! -z ${DOVECOT_MASTER_USER} ]] && [[ ! -z ${DOVECOT_MASTER_PASS} ]]; then + RAND_USER=${DOVECOT_MASTER_USER} + RAND_PASS=${DOVECOT_MASTER_PASS} +fi +echo ${RAND_USER}@mailcow.local:{SHA1}$(echo -n ${RAND_PASS} | sha1sum | awk '{print $1}'):::::: > /etc/dovecot/dovecot-master.passwd +echo ${RAND_USER}@mailcow.local::5000:5000:::: > /etc/dovecot/dovecot-master.userdb +echo ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/sieve.creds + +if [[ -z ${MAILDIR_SUB} ]]; then + MAILDIR_SUB_SHARED= +else + MAILDIR_SUB_SHARED=/${MAILDIR_SUB} +fi +cat < /etc/dovecot/shared_namespace.conf +# Autogenerated by mailcow +namespace { + type = shared + separator = / + prefix = Shared/%%u/ + location = maildir:%%h${MAILDIR_SUB_SHARED}:INDEX=~${MAILDIR_SUB_SHARED}/Shared/%%u + subscriptions = no + list = children +} +EOF + + +cat < /etc/dovecot/sogo_trusted_ip.conf +# Autogenerated by mailcow +remote ${IPV4_NETWORK}.248 { + disable_plaintext_auth = no +} +EOF + +# Create random master Password for SOGo SSO +RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1) +echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass +# Creating additional creds file for SOGo notify crons (calendars, etc) +echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds +cat < /etc/dovecot/sogo-sso.conf +# Autogenerated by mailcow +passdb { + driver = static + args = allow_real_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS} +} +EOF + +if [[ "${MASTER}" =~ ^([nN][oO]|[nN])+$ ]]; then + # Toggling MASTER will result in a rebuild of containers, so the quota script will be recreated + cat <<'EOF' > /usr/local/bin/quota_notify.py +#!/usr/bin/python3 +import sys +sys.exit() +EOF +fi + +# Set mail_replica for HA setups +if [[ -n ${MAILCOW_REPLICA_IP} && -n ${DOVEADM_REPLICA_PORT} ]]; then + cat < /etc/dovecot/mail_replica.conf +# Autogenerated by mailcow +mail_replica = tcp:${MAILCOW_REPLICA_IP}:${DOVEADM_REPLICA_PORT} +EOF +fi + +# Setting variables for indexer-worker inside fts.conf automatically according to mailcow.conf settings +if [[ "${SKIP_FTS}" =~ ^([nN][oO]|[nN])+$ ]]; then + echo -e "\e[94mConfiguring FTS Settings...\e[0m" + echo -e "\e[94mSetting FTS Memory Limit (per process) to ${FTS_HEAP} MB\e[0m" + sed -i "s/vsz_limit\s*=\s*[0-9]*\s*MB*/vsz_limit=${FTS_HEAP} MB/" /etc/dovecot/conf.d/fts.conf + echo -e "\e[94mSetting FTS Process Limit to ${FTS_PROCS}\e[0m" + sed -i "s/process_limit\s*=\s*[0-9]*/process_limit=${FTS_PROCS}/" /etc/dovecot/conf.d/fts.conf +fi + +# 401 is user dovecot +if [[ ! -s /mail_crypt/ecprivkey.pem || ! -s /mail_crypt/ecpubkey.pem ]]; then + openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem + openssl pkey -in /mail_crypt/ecprivkey.pem -pubout -out /mail_crypt/ecpubkey.pem + chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem +else + chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem +fi + +# Fix OpenSSL 3.X TLS1.0, 1.1 support (https://community.mailcow.email/d/4062-hi-all/20) +if grep -qE 'ssl_min_protocol\s*=\s*(TLSv1|TLSv1\.1)\s*$' /etc/dovecot/dovecot.conf /etc/dovecot/extra.conf; then + sed -i '/\[openssl_init\]/a ssl_conf = ssl_configuration' /etc/ssl/openssl.cnf + + echo "[ssl_configuration]" >> /etc/ssl/openssl.cnf + echo "system_default = tls_system_default" >> /etc/ssl/openssl.cnf + echo "[tls_system_default]" >> /etc/ssl/openssl.cnf + echo "MinProtocol = TLSv1" >> /etc/ssl/openssl.cnf + echo "CipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf +fi + +# Compile sieve scripts +sievec /var/vmail/sieve/global_sieve_before.sieve +sievec /var/vmail/sieve/global_sieve_after.sieve +sievec /usr/lib/dovecot/sieve/report-spam.sieve +sievec /usr/lib/dovecot/sieve/report-ham.sieve + +# Fix permissions +chown root:root /etc/dovecot/sql/*.conf +chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* /etc/dovecot/auth/passwd-verify.lua +chmod 640 /etc/dovecot/sql/*.conf /etc/dovecot/auth/passwd-verify.lua +chown -R vmail:vmail /var/vmail/sieve +chown -R vmail:vmail /var/volatile +chown -R vmail:vmail /var/vmail_index +adduser vmail tty +chmod g+rw /dev/console +chown root:tty /dev/console +chmod +x /usr/lib/dovecot/sieve/rspamd-pipe-ham \ + /usr/lib/dovecot/sieve/rspamd-pipe-spam \ + /usr/local/bin/imapsync_runner.pl \ + /usr/local/bin/imapsync \ + /usr/local/bin/trim_logs.sh \ + /usr/local/bin/sa-rules.sh \ + /usr/local/bin/clean_q_aged.sh \ + /usr/local/bin/maildir_gc.sh \ + /usr/local/sbin/stop-supervisor.sh \ + /usr/local/bin/quota_notify.py \ + /usr/local/bin/repl_health.sh \ + /usr/local/bin/optimize-fts.sh + +# Prepare environment file for cronjobs +printenv | sed 's/^\(.*\)$/export \1/g' > /source_env.sh + +# Clean old PID if any +[[ -f /var/run/dovecot/master.pid ]] && rm /var/run/dovecot/master.pid + +# Clean stopped imapsync jobs +rm -f /tmp/imapsync_busy.lock +IMAPSYNC_TABLE=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'imapsync'" -Bs) +[[ ! -z ${IMAPSYNC_TABLE} ]] && mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "UPDATE imapsync SET is_running='0'" + +# Envsubst maildir_gc +echo "$(envsubst < /usr/local/bin/maildir_gc.sh)" > /usr/local/bin/maildir_gc.sh + +# GUID generation +while [[ ${VERSIONS_OK} != 'OK' ]]; do + if [[ ! -z $(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = \"${DBNAME}\" AND TABLE_NAME = 'versions'") ]]; then + VERSIONS_OK=OK + else + echo "Waiting for versions table to be created..." + sleep 3 + fi +done +PUBKEY_MCRYPT=$(doveconf -P 2> /dev/null | grep -i mail_crypt_global_public_key | cut -d '<' -f2) +if [ -f ${PUBKEY_MCRYPT} ]; then + GUID=$(cat <(echo ${MAILCOW_HOSTNAME}) /mail_crypt/ecpubkey.pem | sha256sum | cut -d ' ' -f1 | tr -cd "[a-fA-F0-9.:/] ") + if [ ${#GUID} -eq 64 ]; then + mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF +REPLACE INTO versions (application, version) VALUES ("GUID", "${GUID}"); +EOF + else + mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF +REPLACE INTO versions (application, version) VALUES ("GUID", "INVALID"); +EOF + fi +fi + +# Collect SA rules once now +/usr/local/bin/sa-rules.sh + +# Run hooks +for file in /hooks/*; do + if [ -x "${file}" ]; then + echo "Running hook ${file}" + "${file}" + fi +done + +# For some strange, unknown and stupid reason, Dovecot may run into a race condition, when this file is not touched before it is read by dovecot/auth +# May be related to something inside Docker, I seriously don't know +touch /etc/dovecot/auth/passwd-verify.lua + +if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then + cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf +fi + +exec "$@" diff --git a/mailcow/data/Dockerfiles/dovecot/imapsync b/mailcow/data/Dockerfiles/dovecot/imapsync new file mode 100755 index 0000000..de63d65 --- /dev/null +++ b/mailcow/data/Dockerfiles/dovecot/imapsync @@ -0,0 +1,20539 @@ +#!/usr/bin/env perl + +# $Id: imapsync,v 2.178 2022/01/12 21:28:37 gilles Exp gilles $ +# structure +# pod documentation +# use pragmas +# main program +# global variables initialization +# get_options( ) ; +# default values +# folder loop +# subroutines +# sub usage + + +# pod documentation + +=pod + +=head1 NAME + +imapsync - Email IMAP tool for syncing, copying, migrating +and archiving email mailboxes between two imap servers, one way, +and without duplicates. + +=head1 VERSION + +This documentation refers to Imapsync $Revision: 2.178 $ + +=head1 USAGE + + To synchronize the source imap account + "test1" on server "test1.lamiral.info" with password "secret1" + to the destination imap account + "test2" on server "test2.lamiral.info" with password "secret2" + do: + + imapsync \ + --host1 test1.lamiral.info --user1 test1 --password1 secret1 \ + --host2 test2.lamiral.info --user2 test2 --password2 secret2 + +=head1 DESCRIPTION + +We sometimes need to transfer mailboxes from one imap server to +one another. + +Imapsync command is a tool allowing incremental and +recursive imap transfers from one mailbox to another. +If you don't understand the previous sentence, it's normal, +it's pedantic computer-oriented jargon. + +All folders are transferred, recursively, meaning +the whole folder hierarchy is taken, all messages in them, +and all message flags (\Seen \Answered \Flagged etc.) +are synced too. + +Imapsync reduces the amount of data transferred by not transferring +a given message if it already resides on the destination side. +Messages that are on the destination side but not on the +source side stay as they are. See the --delete2 +option to have strict sync and delete them. + +How imapsync know a message is already on both sides? +Same specific headers and the transfer is done only once. +By default, the identification headers are +"Message-Id:" and "Received:" lines +but this choice can be changed with the --useheader option, +most often a duplicate problem is solved by using +--useheader "Message-Id" + + +All flags are preserved, unread messages will stay unread, +read ones will stay read, deleted will stay deleted. +In the IMAP protocol, a deleted message is not really deleted, +it is marked \Deleted and can be undelete. Real destruction +comes with the EXPUNGE or UIDEXPUNGE IMAP commands. + +You can abort the transfer at any time and restart it later, +imapsync works well with bad connections and interruptions, +by design. On a terminal hit Ctr-c twice within two seconds +to abort the program. Hit Ctr-c just once makes +imapsync reconnect to both imap servers. + +How do you know the sync is finished and well done? +When imapsync ends by itself it mentions it with lines like those: + + Exiting with return value 0 (EX_OK: successful termination) 0/50 nb_errors/max_errors PID 301 + Removing pidfile /tmp/imapsync.pid + Log file is LOG_imapsync/2020_11_17_15_59_22_761_test1_test2.txt ( to change it, use --logfile filepath ; or use --nolog to turn off logging ) + +If you don't have those lines it means that either the sync process is still +running (or eventually hanging indefinitely) or that it ended without +a whisper, a strong kill -9 on Linux for example. + +If you have those final lines then it means the sync process is properly +finished. It may have encountered problems though. + +A good synchronization is mentioned by some lines above the last ones, especially +those three lines: + + The sync looks good, all 1745 identified messages in host1 are on host2. + There is no unidentified message on host1. + Detected 0 errors + + +A classical scenario is synchronizing a mailbox B from another mailbox A +where you just want to keep a strict copy of A in B. Strict meaning +all messages in A will be in B but no more. + +For this, option --delete2 can be used, it deletes messages in the host2 +folder B that are not in the host1 folder A. If you also need to destroy +host2 folders that are not in host1 then use --delete2folders. See also +--delete2foldersonly and --delete2foldersbutnot to set up exceptions +on folders to destroy. INBOX will never be destroyed, it's a mandatory +folder in IMAP so imapsync doesn't even try to remove it. + +A different scenario is to delete the messages from the source mailbox +after a successful transfer, it can be a good feature when migrating +mailboxes since messages will be only on one side. The source account +will only have messages that are not on the destination yet, ie, +messages that arrived after a sync or that failed to be transferred. + +In that case, use the --delete1 option. Option --delete1 implies also +the option --expunge1 so all messages marked deleted on host1 will be +deleted. In IMAP protocol deleting a message does not delete it, +it marks it with the flag \Deleted, allowing an undelete. Expunging +a folder removes, definitively, all the messages marked as \Deleted +in this folder. + +You can also decide to remove empty folders once all of their messages +have been transferred. Add --delete1emptyfolders to obtain this +behavior. + + +Imapsync is not adequate for maintaining two active imap accounts +in synchronization when the user plays independently on both sides. +Use offlineimap (written by John Goerzen) or mbsync (written by +Michael R. Elkins) for a 2 ways synchronization. + + +=head1 OPTIONS + + usage: imapsync [options] + +The standard options are the six values forming the credentials. +Three values on each side are needed in order to login into the IMAP +servers. These six values are a hostname, a username, and a password, two times. + +Conventions used in the following descriptions of the options: + + str means string + int means integer number + flo means float number + reg means regular expression + cmd means command + + --dry : Makes imapsync doing nothing for real; it just print what + would be done without --dry. + +=head2 OPTIONS/credentials + + + --host1 str : Source or "from" imap server. + --port1 int : Port to connect on host1. + Optional since default ports are the + well known ports imap/143 or imaps/993. + --user1 str : User to login on host1. + --password1 str : Password of user1. + + --host2 str : "destination" imap server. + --port2 int : Port to connect on host2. Optional + --user2 str : User to login on host2. + --password2 str : Password of user2. + + --showpasswords : Shows passwords on output instead of "MASKED". + Useful to restart a complete run by just reading + the command line used in the log, + or to debug passwords. + It's not a secure practice at all! + + --passfile1 str : Password file for the user1. It must contain the + password on the first line. This option avoids showing + the password on the command line like --password1 does. + --passfile2 str : Password file for the user2. + +You can also pass the passwords in the environment variables +IMAPSYNC_PASSWORD1 and IMAPSYNC_PASSWORD2. If you don't pass +the user1 password via --password1 nor --passfile1 nor $IMAPSYNC_PASSWORD1 +then imapsync will prompt to enter the password on the terminal. +Same thing for user2 password. + +=head2 OPTIONS/encryption + + --nossl1 : Do not use a SSL connection on host1. + --ssl1 : Use a SSL connection on host1. On by default if possible. + + --nossl2 : Do not use a SSL connection on host2. + --ssl2 : Use a SSL connection on host2. On by default if possible. + + --notls1 : Do not use a TLS connection on host1. + --tls1 : Use a TLS connection on host1. On by default if possible. + + --notls2 : Do not use a TLS connection on host2. + --tls2 : Use a TLS connection on host2. On by default if possible. + + --debugssl int : SSL debug mode from 0 to 4. + + --sslargs1 str : Pass any ssl parameter for host1 ssl or tls connection. Example: + --sslargs1 SSL_verify_mode=1 --sslargs1 SSL_version=SSLv3 + See all possibilities in the new() method of IO::Socket::SSL + http://search.cpan.org/perldoc?IO::Socket::SSL#Description_Of_Methods + --sslargs2 str : Pass any ssl parameter for host2 ssl or tls connection. + See --sslargs1 + + +=head2 OPTIONS/authentication + + --authmech1 str : Auth mechanism to use with host1: + PLAIN, LOGIN, CRAM-MD5 etc. Use UPPERCASE. + --authmech2 str : Auth mechanism to use with host2. See --authmech1 + + --authuser1 str : User to auth with on host1 (admin user). + Avoid using --authmech1 SOMETHING with --authuser1. + --authuser2 str : User to auth with on host2 (admin user). + --proxyauth1 : Use proxyauth on host1. Requires --authuser1. + Required by Sun/iPlanet/Netscape IMAP servers to + be able to use an administrative user. + --proxyauth2 : Use proxyauth on host2. Requires --authuser2. + + --authmd51 : Use MD5 authentication for host1. + --authmd52 : Use MD5 authentication for host2. + --domain1 str : Domain on host1 (NTLM authentication). + --domain2 str : Domain on host2 (NTLM authentication). + + --oauthaccesstoken1 str : The access token to authenticate with OAUTH2. + It will be combined with the --user1 value to form the + string to pass with XOAUTH2 authentication. + The password given by --password1 or --passfile1 + is ignored. + Instead of the access token itself, the value can be a + file containing the access token on the first line. + If the value is a file, imapsync reads its first line + and take this line as the access token. The advantage + of the file is that if the access token changes then + imapsync can read it again when it needs to reconnect + during a run. + + + --oauthaccesstoken2 str : same thing as --oauthaccesstoken1 + + --oauthdirect1 str : The direct string to pass with XOAUTH2 authentication. + The password given by --password1 or --passfile1 and + the user given by --user1 are ignored. + + --oauthdirect2 str : same thing as oauthdirect1 + + +=head2 OPTIONS/folders + + + --folder str : Sync this folder. + --folder str : and this one, etc. + --folderrec str : Sync this folder recursively. + --folderrec str : and this one, etc. + + --folderfirst str : Sync this folder first. Ex. --folderfirst "INBOX" + --folderfirst str : then this one, etc. + --folderlast str : Sync this folder last. --folderlast "[Gmail]/All Mail" + --folderlast str : then this one, etc. + + --nomixfolders : Do not merge folders when host1 is case-sensitive + while host2 is not (like Exchange). Only the first + similar folder is synced (example: with folders + "Sent", "SENT" and "sent" + on host1 only "Sent" will be synced to host2). + + --skipemptyfolders : Empty host1 folders are not created on host2. + + --include reg : Sync folders matching this regular expression + --include reg : or this one, etc. + If both --include --exclude options are used, then + include is done before. + --exclude reg : Skips folders matching this regular expression + Several folders to avoid: + --exclude 'fold1|fold2|f3' skips fold1, fold2 and f3. + --exclude reg : or this one, etc. + + --automap : guesses folders mapping, for folders well known as + "Sent", "Junk", "Drafts", "All", "Archive", "Flagged". + + --f1f2 str1=str2 : Force folder str1 to be synced to str2, + --f1f2 overrides --automap and --regextrans2. + Use several --f1f2 options to map several folders. + Option --f1f2 is a one to one only folder mapping, + str1 and str2 have to be full path folder names. + + --subfolder2 str : Syncs the whole host1 folders hierarchy under the + host2 folder named str. + It does it internally by adding three + --regextrans2 options before all others. + Add --debug to see what's really going on. + + --subfolder1 str : Syncs the host1 folders hierarchy which is under folder + str to the root hierarchy of host2. + It's the couterpart of a sync done by --subfolder2 + when doing it in the reverse order. + Backup/Restore scenario: + Use --subfolder2 str for a backup to the folder str + on host2. Then use --subfolder1 str for restoring + from the folder str, after inverting + host1/host2 user1/user2 values. + + + --subscribed : Transfers subscribed folders. + --subscribe : Subscribe to the folders transferred on the + host2 that are subscribed on host1. On by default. + --subscribeall : Subscribe to the folders transferred on the + host2 even if they are not subscribed on host1. + + --prefix1 str : Remove prefix str to all destination folders, + usually "INBOX." or "INBOX/" or an empty string "". + imapsync guesses the prefix if host1 imap server + does not have NAMESPACE capability. So this option + should not be used most of the time. + --prefix2 str : Add prefix to all host2 folders. See --prefix1 + + --sep1 str : Host1 separator. This option should not be used + most of the time. + Imapsync gets the separator from the server itself, + by using NAMESPACE, or it tries to guess it + from the folders listing (it counts + characters / . \\ \ in folder names and choose the + more frequent, or finally / if nothing is found. + --sep2 str : Host2 separator. See --sep1 + + --regextrans2 reg : Apply the whole regex to each destination folders. + --regextrans2 reg : and this one. etc. + When you play with the --regextrans2 option, first + add also the safe options --dry --justfolders + Then, when happy, remove --dry for a run, then + remove --justfolders for the next ones. + Have in mind that --regextrans2 is applied after + the automatic prefix and separator inversion. + For examples see: + https://imapsync.lamiral.info/FAQ.d/FAQ.Folders_Mapping.txt + +=head2 OPTIONS/folders sizes + + --nofoldersizes : Do not calculate the size of each folder at the + beginning of the sync. Default is to calculate them. + --nofoldersizesatend: Do not calculate the size of each folder at the + end of the sync. Default is to calculate them. + --justfoldersizes : Exit after having printed the initial folder sizes. + + +=head2 OPTIONS/tmp + + + --tmpdir str : Where to store temporary files and subdirectories. + Will be created if it doesn't exist. + Default is system specific, Unix is /tmp but + /tmp is often too small and deleted at reboot. + --tmpdir /var/tmp should be better. + + --pidfile str : The file where imapsync pid is written, + it can be dirname/filename complete path. + The default name is imapsync.pid in tmpdir. + + --pidfilelocking : Abort if pidfile already exists. Useful to avoid + concurrent transfers on the same mailbox. + + +=head2 OPTIONS/log + + --nolog : Turn off logging on file + --logfile str : Change the default log filename (can be dirname/filename). + --logdir str : Change the default log directory. Default is LOG_imapsync/ + +The default logfile name is for example + + LOG_imapsync/2019_12_22_23_57_59_532_user1_user2.txt + +where: + + 2019_12_22_23_57_59_532 is nearly the date of the start + YYYY_MM_DD_HH_MM_SS_mmm + year_month_day_hour_minute_seconde_millisecond + +and user1 user2 are the --user1 --user2 values. + +=head2 OPTIONS/messages + + --skipmess reg : Skips messages matching the regex. + Example: 'm/[\x80-\xff]/' # to avoid 8bits messages. + --skipmess is applied before --regexmess + --skipmess reg : or this one, etc. + + --skipcrossduplicates : Avoid copying messages that are already copied + in another folder, good from Gmail to XYZ when + XYZ is not also Gmail. + Activated with --gmail1 unless --noskipcrossduplicates + + --debugcrossduplicates : Prints which messages (UIDs) are skipped with + --skipcrossduplicates and in what other folders + they are. + + --pipemess cmd : Apply this cmd command to each message content + before the copy. + --pipemess cmd : and this one, etc. + With several --pipemess, the output of each cmd + command (STDOUT) is given to the input (STDIN) + of the next command. + For example, + --pipemess cmd1 --pipemess cmd2 --pipemess cmd3 + is like a Unix pipe: + "cat message | cmd1 | cmd2 | cmd3" + + --disarmreadreceipts : Disarms read receipts (host2 Exchange issue) + + --regexmess reg : Apply the whole regex to each message before transfer. + Example: 's/\000/ /g' # to replace null characters + by spaces. + --regexmess reg : and this one, etc. + + --truncmess int : truncates messages when their size exceed the int + value, specified in bytes. Good to sync too big + messages or to "suppress" attachments. + Have in mind that this way, messages become + uncoherent somehow. + +=head2 OPTIONS/labels + +Gmail present labels as folders in imap. Imapsync can accelerate the sync +by syncing X-GM-LABELS, it will avoid to transfer messages when they are +already on host2 in another folder. + + + --synclabels : Syncs also Gmail labels when a message is copied to host2. + Activated by default with --gmail1 --gmail2 unless + --nosynclabels is added. + + --resynclabels : Resyncs Gmail labels when a message is already on host2. + Activated by default with --gmail1 --gmail2 unless + --noresynclabels is added. + +For Gmail syncs, see also: +https://imapsync.lamiral.info/FAQ.d/FAQ.Gmail.txt + +=head2 OPTIONS/flags + + If you encounter flag problems see also: + https://imapsync.lamiral.info/FAQ.d/FAQ.Flags.txt + + --regexflag reg : Apply the whole regex to each flags list. + Example: 's/"Junk"//g' # to remove "Junk" flag. + --regexflag reg : then this one, etc. + + --resyncflags : Resync flags for already transferred messages. + On by default. + --noresyncflags : Do not resync flags for already transferred messages. + May be useful when a user has already started to play + with its host2 account. + + --filterbuggyflags : Filter flags known to be buggy and generators of errors + "BAD Invalid system flag" or "NO APPEND Invalid flag list". + +=head2 OPTIONS/deletions + + --delete1 : Deletes messages on host1 server after a successful + transfer. Option --delete1 has the following behavior: + it marks messages as deleted with the IMAP flag + \Deleted, then messages are really deleted with an + EXPUNGE IMAP command. If expunging after each message + slows down too much the sync then use + --noexpungeaftereach to speed up, expunging will then be + done only twice per folder, one at the beginning and + one at the end of a folder sync. + + --expunge1 : Expunge messages on host1 just before syncing a folder. + Expunge is done per folder. + Expunge aims is to really delete messages marked deleted. + An expunge is also done after each message copied + if option --delete1 is set (unless --noexpungeaftereach). + + --noexpunge1 : Do not expunge messages on host1. + + --delete1emptyfolders : Deletes empty folders on host1, INBOX excepted. + Useful with --delete1 since what remains on host1 + is only what failed to be synced. + + --delete2 : Delete messages in the host2 account that are not in + the host1 account. Useful for backup or pre-sync. + --delete2 implies --uidexpunge2 + + --delete2duplicates : Deletes messages in host2 that are duplicates in host2. + Works only without --useuid since duplicates are + detected with an header part of each message. + NB: --delete2duplicates is far less violent than --delete2 + since it removes only duplicates. + + --delete2folders : Delete folders in host2 that are not in host1. + For safety, first try it like this, it is safe: + --delete2folders --dry --justfolders --nofoldersizes + and see what folders will be deleted. + + --delete2foldersonly reg : Delete only folders matching the regex reg. + Example: --delete2foldersonly "/^Junk$|^INBOX.Junk$/" + This option activates --delete2folders + + --delete2foldersbutnot reg : Do not delete folders matching the regex rex. + Example: --delete2foldersbutnot "/Tasks$|Contacts$|Foo$/" + This option activates --delete2folders + + --noexpunge2 : Do not expunge messages on host2. + --nouidexpunge2 : Do not uidexpunge messages on the host2 account + that are not on the host1 account. + + +=head2 OPTIONS/dates + + If you encounter problems with dates, see also: + https://imapsync.lamiral.info/FAQ.d/FAQ.Dates.txt + + --syncinternaldates : Sets the internal dates on host2 as the same as host1. + Turned on by default. Internal date is the date + a message arrived on a host (Unix mtime usually). + --idatefromheader : Sets the internal dates on host2 as same as the + ones in "Date:" headers. + + + +=head2 OPTIONS/message selection + + --maxsize int : Skip messages larger (or equal) than int bytes + --minsize int : Skip messages smaller (or equal) than int bytes + + --maxage int : Skip messages older than int days. + final stats (skipped) don't count older messages + see also --minage + --minage int : Skip messages newer than int days. + final stats (skipped) don't count newer messages + You can do (+ zone are the messages selected): + past|----maxage+++++++++++++++>now + past|+++++++++++++++minage---->now + past|----maxage+++++minage---->now (intersection) + past|++++minage-----maxage++++>now (union) + + --search str : Selects only messages returned by this IMAP SEARCH + command. Applied on both sides. + For a complete set of what can be search see + https://imapsync.lamiral.info/FAQ.d/FAQ.Messages_Selection.txt + + --search1 str : Same as --search but for selecting host1 messages only. + --search2 str : Same as --search but for selecting host2 messages only. + So --search CRIT equals --search1 CRIT --search2 CRIT + + --noabletosearch : Makes --minage and --maxage options use the internal + dates given by a FETCH imap command instead of the + "Date:" header. Internal date is the arrival date + in the mailbox. + --noabletosearch equals --noabletosearch1 --noabletosearch2 + + --noabletosearch1 : Like --noabletosearch but for host1 only. + --noabletosearch2 : Like --noabletosearch but for host2 only. + + --maxlinelength int : skip messages with a line length longer than int bytes. + RFC 2822 says it must be no more than 1000 bytes but + real life servers and email clients do more. + + + --useheader str : Use this header to compare messages on both sides. + Example: "Message-Id" or "Received" or "Date". + --useheader str and this one, etc. + + --syncduplicates : Sync also duplicates. Off by default. + + --usecache : Use cache to speed up next syncs. Off by default. + --nousecache : Do not use cache. Caveat: --useuid --nousecache creates + duplicates on multiple runs. + + --useuid : Use UIDs instead of headers as a criterion to recognize + messages. Option --usecache is then implied unless + --nousecache is used. + + +=head2 OPTIONS/miscellaneous + + --syncacls : Synchronizes acls (Access Control Lists). + Acls in IMAP are not standardized, be careful + since one acl code on one side may signify something + else on the other one. + --nosyncacls : Does not synchronize acls. This is the default. + + --addheader : When a message has no headers to be identified, + --addheader adds a "Message-Id" header, + like "Message-Id: 12345@imapsync", where 12345 + is the imap UID of the message on the host1 folder. + Useful to sync folders "Sent" or "Draft". + + +=head2 OPTIONS/debugging + + --debug : Debug mode. + --debugfolders : Debug mode for the folders part only. + --debugcontent : Debug content of the messages transferred. Huge output. + --debugflags : Debug mode for flags. + --debugimap1 : IMAP debug mode for host1. Very verbose. + --debugimap2 : IMAP debug mode for host2. Very verbose. + --debugimap : IMAP debug mode for host1 and host2. Twice very verbose. + --debugmemory : Debug mode showing memory consumption after each copy. + + --errorsmax int : Exit when int number of errors is reached. Default is 50. + + --tests : Run local non-regression tests. Exit code 0 means all ok. + --testslive : Run a live test with test1.lamiral.info imap server. + Useful to check the basics. Needs internet connection. + --testslive6 : Run a live test with ks6ipv6.lamiral.info imap server. + Useful to check the ipv6 connectivity. Needs internet. + + +=head2 OPTIONS/specific + + --gmail1 : sets --host1 to Gmail and other options. See FAQ.Gmail.txt + --gmail2 : sets --host2 to Gmail and other options. See FAQ.Gmail.txt + + --office1 : sets --host1 to Office365 and other options. See FAQ.Office365.txt + --office2 : sets --host2 to Office365 and other options. See FAQ.Office365.txt + + --exchange1 : sets options for Exchange. See FAQ.Exchange.txt + --exchange2 : sets options for Exchange. See FAQ.Exchange.txt + + --domino1 : sets options for Domino. See FAQ.Domino.txt + --domino2 : sets options for Domino. See FAQ.Domino.txt + + +=head2 OPTIONS/behavior + + --timeout1 flo : Connection timeout in seconds for host1. + Default is 120 and 0 means no timeout at all. + --timeout2 flo : Connection timeout in seconds for host2. + Default is 120 and 0 means no timeout at all. + + Caveat, under CGI context, you may encounter a timeout + from the webserver, killing imapsync and the imap connexions. + See the document INSTALL.OnlineUI.txt and search + for "Timeout" for how to deal with this issue. + + --keepalive1 : https://metacpan.org/pod/Mail::IMAPClient#Keepalive + Some firewalls and network gears like to timeout connections + prematurely if the connection sits idle. + This option enables SO_KEEPALIVE on the host1 socket. + --keepalive1 is on by default since imapsync release 2.169 + Use --nokeepalive1 to disable it. + + --keepalive2 : Same as --keepalive2 but for host2. + Use --nokeepalive2 to disable it. + + --maxmessagespersecond flo : limits the average number of messages + transferred per second. + + --maxbytespersecond int : limits the average transfer rate per second. + --maxbytesafter int : starts --maxbytespersecond limitation only after + --maxbytesafter amount of data transferred. + + --maxsleep flo : do not sleep more than int seconds. + On by default, 2 seconds max, like --maxsleep 2 + + --abort : terminates a previous call still running. + It uses the pidfile to know what process to abort. + + --exitwhenover int : Stop syncing and exits when int total bytes + transferred is reached. + + --version : Print only the software version. + --noreleasecheck : Do not check for any new imapsync release. + --releasecheck : Check for new imapsync release. + it's an http request to + http://imapsync.lamiral.info/prj/imapsync/VERSION + + --noid : Do not send/receive IMAP "ID" command to imap servers. + + --justconnect : Just connect to both servers and print useful + information. Need only --host1 and --host2 options. + Obsolete since "imapsync --host1 imaphost" alone + implies --justconnect + + --justlogin : Just login to both host1 and host2 with users + credentials, then exit. + + --justfolders : Do only things about folders (ignore messages). + + --help : print this help. + + Example: to synchronize imap account "test1" on "test1.lamiral.info" + to imap account "test2" on "test2.lamiral.info" + with test1 password "secret1" + and test2 password "secret2" + + imapsync \ + --host1 test1.lamiral.info --user1 test1 --password1 secret1 \ + --host2 test2.lamiral.info --user2 test2 --password2 secret2 + + +=cut +# comment + +=pod + + + +=head1 SECURITY + +You can use --passfile1 instead of --password1 to mention the +password since it is safer. With --password1 option, on Linux, +any user on your host can see the password by using the 'ps auxwwww' +command. Using a variable (like IMAPSYNC_PASSWORD1) is also +dangerous because of the 'ps auxwwwwe' command. So, saving +the password in a well protected file (600 or rw-------) is +the best solution. + +Imapsync activates ssl or tls encryption by default, if possible. + +What detailed behavior is under this "if possible"? + +Imapsync activates ssl if the well known port imaps port (993) is open +on the imap servers. If the imaps port is closed then it open a +normal (clear) connection on port 143 but it looks for TLS support +in the CAPABILITY list of the servers. If TLS is supported +then imapsync goes to encryption with STARTTLS. + +If the automatic ssl and the tls detections fail then imapsync will +not protect against sniffing activities on the network, especially +for passwords. + +If you want to force ssl or tls just use --ssl1 --ssl2 or --tls1 --tls2 + +See also the document FAQ.Security.txt in the FAQ.d/ directory +or at https://imapsync.lamiral.info/FAQ.d/FAQ.Security.txt + +=head1 EXIT STATUS + +Imapsync will exit with a 0 status (return code) if everything went good. +Otherwise, it exits with a non-zero status. That's classical Unix behavior. +Here is the list of the exit code values (an integer between 0 and 255). +In Bourne Shells, this exit code value can be retrieved within the variable +value "$?" if you read it just after the imapsync call. + +The names reflect their meaning: + +=for comment +egrep '^Readonly my.*\$EX' imapsync | egrep -o 'EX.*' | sed 's_^_ _' + + EX_OK => 0 ; #/* successful termination */ + EX_USAGE => 64 ; #/* command line usage error */ + EX_NOINPUT => 66 ; #/* cannot open input */ + EX_UNAVAILABLE => 69 ; #/* service unavailable */ + EX_SOFTWARE => 70 ; #/* internal software error */ + EXIT_CATCH_ALL => 1 ; # Any other error + EXIT_BY_SIGNAL => 6 ; # Should be 128+n where n is the sig_num + EXIT_BY_FILE => 7 ; + EXIT_PID_FILE_ERROR => 8 ; + EXIT_CONNECTION_FAILURE => 10 ; + EXIT_TLS_FAILURE => 12 ; + EXIT_AUTHENTICATION_FAILURE => 16 ; + EXIT_SUBFOLDER1_NO_EXISTS => 21 ; + EXIT_WITH_ERRORS => 111 ; + EXIT_WITH_ERRORS_MAX => 112 ; + EXIT_OVERQUOTA => 113 ; + EXIT_ERR_APPEND => 114 ; + EXIT_ERR_FETCH => 115 ; + EXIT_ERR_CREATE => 116 ; + EXIT_ERR_SELECT => 117 ; + EXIT_TRANSFER_EXCEEDED => 118 ; + EXIT_ERR_APPEND_VIRUS => 119 ; + EXIT_TESTS_FAILED => 254 ; # Like Test::More API + EXIT_CONNECTION_FAILURE_HOST1 => 101 ; + EXIT_CONNECTION_FAILURE_HOST2 => 102 ; + EXIT_AUTHENTICATION_FAILURE_USER1 => 161 ; + EXIT_AUTHENTICATION_FAILURE_USER2 => 162 ; + + +=head1 LICENSE AND COPYRIGHT + +Imapsync is free, open, public but not always gratis software +cover by the NOLIMIT Public License, now called NLPL. +See the LICENSE file included in the distribution or just read this +simple sentence as it IS the licence text: + + "No limits to do anything with this work and this license." + +In case it is not long enough, I repeat: + + "No limits to do anything with this work and this license." + +Look at https://imapsync.lamiral.info/LICENSE + +=head1 AUTHOR + +Gilles LAMIRAL + +Good feedback is always welcome. +Bad feedback is very often welcome. + +Gilles LAMIRAL earns his living by writing, installing, +configuring and sometimes teaching free, open and often gratis +software. Imapsync used to be "always gratis" but now it is +only "often gratis" because imapsync is sold by its author, +your servitor, a good way to maintain and support free open public +software tools over decades. + +=head1 BUGS AND LIMITATIONS + +See https://imapsync.lamiral.info/FAQ.d/FAQ.Reporting_Bugs.txt + +=head1 IMAP SERVERS supported + +See https://imapsync.lamiral.info/S/imapservers.shtml + +=head1 HUGE MIGRATION + +If you have many mailboxes to migrate think about a little +shell program. Write a file called file.txt (for example) +containing users and passwords. +The separator used in this example is ';' + +The file.txt file contains: + +user001_1;password001_1;user001_2;password001_2 +user002_1;password002_1;user002_2;password002_2 +user003_1;password003_1;user003_2;password003_2 +user004_1;password004_1;user004_2;password004_2 +user005_1;password005_1;user005_2;password005_2 +... + +On Unix the shell program can be: + + { while IFS=';' read u1 p1 u2 p2; do + imapsync --host1 imap.side1.org --user1 "$u1" --password1 "$p1" \ + --host2 imap.side2.org --user2 "$u2" --password2 "$p2" ... + done ; } < file.txt + +On Windows the batch program can be: + + FOR /F "tokens=1,2,3,4 delims=; eol=#" %%G IN (file.txt) DO imapsync ^ + --host1 imap.side1.org --user1 %%G --password1 %%H ^ + --host2 imap.side2.org --user2 %%I --password2 %%J ... + +The ... have to be replaced by nothing or any imapsync option. +Welcome in shell or batch programming ! + +You will find already written scripts at +https://imapsync.lamiral.info/examples/ + +=head1 INSTALL + + Imapsync works under any Unix with Perl. + + Imapsync works under most Windows (2000, XP, Vista, Seven, Eight, Ten + and all Server releases 2000, 2003, 2008 and R2, 2012 and R2, 2016) + as a standalone binary software called imapsync.exe, + usually launched from a batch file in order to avoid always typing + the options. There is also a 32bit binary called imapsync_32bit.exe + + Imapsync works under OS X as a standalone binary + software called imapsync_bin_Darwin + + Purchase latest imapsync at + https://imapsync.lamiral.info/ + + You'll receive a link to a compressed tarball called imapsync-x.xx.tgz + where x.xx is the version number. Untar the tarball where + you want (on Unix): + + tar xzvf imapsync-x.xx.tgz + + Go into the directory imapsync-x.xx and read the INSTALL file. + As mentioned at https://imapsync.lamiral.info/#install + the INSTALL file can also be found at + https://imapsync.lamiral.info/INSTALL.d/INSTALL.ANY.txt + It is now split in several files for each system + https://imapsync.lamiral.info/INSTALL.d/ + +=head1 CONFIGURATION + +There is no specific configuration file for imapsync, +everything is specified by the command line parameters +and the default behavior. + + +=head1 HACKING + +Feel free to hack imapsync as the NOLIMIT license permits it. + + +=head1 SIMILAR SOFTWARE + + See also https://imapsync.lamiral.info/S/external.shtml + for a better up to date list. + +List verified on Friday July 1, 2021. + + imapsync: https://github.com/imapsync/imapsync (this is an imapsync copy, sometimes delayed, with --noreleasecheck by default since release 1.592, 2014/05/22) + imap_tools: https://web.archive.org/web/20161228145952/http://www.athensfbc.com/imap_tools/. The imap_tools code is now at https://github.com/andrewnimmo/rick-sanders-imap-tools + imaputils: https://github.com/mtsatsenko/imaputils (very old imap_tools fork) + Doveadm-Sync: https://wiki2.dovecot.org/Tools/Doveadm/Sync ( Dovecot sync tool ) + davmail: http://davmail.sourceforge.net/ + offlineimap: http://offlineimap.org/ + fdm: https://github.com/nicm/fdm + mbsync: http://isync.sourceforge.net/ + mailsync: http://mailsync.sourceforge.net/ + mailutil: https://www.washington.edu/imap/ part of the UW IMAP toolkit. (well, seems abandoned now) + imaprepl: https://bl0rg.net/software/ http://freecode.com/projects/imap-repl/ + imapcopy (Pascal): http://www.ardiehl.de/imapcopy/ + imapcopy (Java): https://code.google.com/archive/p/imapcopy/ + imapsize: http://www.broobles.com/imapsize/ + migrationtool: http://sourceforge.net/projects/migrationtool/ + imapmigrate: http://sourceforge.net/projects/cyrus-utils/ + larch: https://github.com/rgrove/larch (derived from wonko_imapsync, good at Gmail) + wonko_imapsync: http://wonko.com/article/554 (superseded by larch) + pop2imap: http://www.linux-france.org/prj/pop2imap/ (I wrote that too) + exchange-away: http://exchange-away.sourceforge.net/ + SyncBackPro: http://www.2brightsparks.com/syncback/sbpro.html + ImapSyncClient: https://github.com/ridaamirini/ImapSyncClient + MailStore: https://www.mailstore.com/en/products/mailstore-home/ + mnIMAPSync: https://github.com/manusa/mnIMAPSync + imap-upload: http://imap-upload.sourceforge.net/ (A tool for uploading a local mbox file to IMAP4 server) + imapbackup: https://github.com/rcarmo/imapbackup (A Python script for incremental backups of IMAP mailboxes) + BitRecover email-backup 99 USD, 299 USD https://www.bitrecover.com/email-backup/. + ImportExportTools: https://addons.thunderbird.net/en-us/thunderbird/addon/importexporttools/ ImportExportTools for Mozilla Thunderbird by Paolo Kaosmos. ImportExportTools does not do IMAP. + rximapmail: https://sourceforge.net/projects/rximapmail/ + CodeTwo: https://www.codetwo.com/ but CodeTwo does imap source to Office365 only. + +=head1 HISTORY + +I initially wrote imapsync in July 2001 because an enterprise, +called BaSystemes, paid me to install a new imap server +without losing huge old mailboxes located in a far +away remote imap server, accessible by an +often broken low-bandwidth ISDN link. + +I had to verify every mailbox was well transferred, all folders, all messages, +without wasting bandwidth or creating duplicates upon resyncs. The imapsync +design was made with the beautiful rsync command in mind. + +Imapsync started its life as a patch of the copy_folder.pl +script. The script copy_folder.pl comes from the Mail-IMAPClient-2.1.3 perl +module tarball source (more precisely in the examples/ directory of the +Mail-IMAPClient tarball). + +So many changes happened since then that I wonder +if it remains any lines of the original +copy_folder.pl in imapsync source code. + + +=cut + + +# use pragmas +# + +use strict ; +use warnings ; +use Carp ; +use Cwd ; +use Compress::Zlib ; +use Data::Dumper ; +use Digest::HMAC_SHA1 qw( hmac_sha1 hmac_sha1_hex ) ; +use Digest::MD5 qw( md5 md5_hex md5_base64 ) ; +use Encode ; +use Encode::IMAPUTF7 ; +use English qw( -no_match_vars ) ; +use Errno qw(EAGAIN EPIPE ECONNRESET) ; +use Fcntl ; +use File::Basename ; +use File::Copy::Recursive ; +use File::Glob qw( :glob ) ; +use File::Path qw( mkpath rmtree ) ; +use File::Spec ; +use File::stat ; +use Getopt::Long ( ) ; +use IO::File ; +use IO::Socket qw( :crlf SOL_SOCKET SO_KEEPALIVE ) ; +use IO::Socket::INET6 ; +use IO::Socket::SSL ; +use IO::Tee ; +use IPC::Open3 'open3' ; +#use locale ; +use Mail::IMAPClient 3.30 ; +use MIME::Base64 ; +use Pod::Usage qw(pod2usage) ; +use POSIX qw( uname SIGALRM :sys_wait_h ) ; +use Sys::Hostname ; +use Term::ReadKey ; +use Test::More ; +use Time::HiRes qw( time sleep ) ; +use Time::Local ; +use Unicode::String ; +use Readonly ; +use Sys::MemInfo ; +use Regexp::Common ; +use Text::ParseWords ; # for quotewords() +use File::Tail ; + + + +local $OUTPUT_AUTOFLUSH = 1 ; + +# constants + +# Let us do like sysexits.h +# /usr/include/sysexits.h +# and https://www.tldp.org/LDP/abs/html/exitcodes.html + +# Should avoid 2 126 127 128..128+64=192 255 +# Should use 0 1 3..125 193..254 + +Readonly my $EX_OK => 0 ; #/* successful termination */ +Readonly my $EX_USAGE => 64 ; #/* command line usage error */ +#Readonly my $EX_DATAERR => 65 ; #/* data format error */ +Readonly my $EX_NOINPUT => 66 ; #/* cannot open input */ +#Readonly my $EX_NOUSER => 67 ; #/* addressee unknown */ +#Readonly my $EX_NOHOST => 68 ; #/* host name unknown */ +Readonly my $EX_UNAVAILABLE => 69 ; #/* service unavailable */ +Readonly my $EX_SOFTWARE => 70 ; #/* internal software error */ +#Readonly my $EX_OSERR => 71 ; #/* system error (e.g., can't fork) */ +#Readonly my $EX_OSFILE => 72 ; #/* critical OS file missing */ +#Readonly my $EX_CANTCREAT => 73 ; #/* can't create (user) output file */ +#Readonly my $EX_IOERR => 74 ; #/* input/output error */ +#Readonly my $EX_TEMPFAIL => 75 ; #/* temp failure; user is invited to retry */ +#Readonly my $EX_PROTOCOL => 76 ; #/* remote error in protocol */ +#Readonly my $EX_NOPERM => 77 ; #/* permission denied */ +#Readonly my $EX_CONFIG => 78 ; #/* configuration error */ + +# Mine +Readonly my $EXIT_CATCH_ALL => 1 ; # Any other error +Readonly my $EXIT_BY_SIGNAL => 6 ; # Should be 128+n where n is the sig_num +Readonly my $EXIT_BY_FILE => 7 ; +Readonly my $EXIT_PID_FILE_ERROR => 8 ; +Readonly my $EXIT_CONNECTION_FAILURE => 10 ; +Readonly my $EXIT_TLS_FAILURE => 12 ; +Readonly my $EXIT_AUTHENTICATION_FAILURE => 16 ; +Readonly my $EXIT_SUBFOLDER1_NO_EXISTS => 21 ; +Readonly my $EXIT_WITH_ERRORS => 111 ; +Readonly my $EXIT_WITH_ERRORS_MAX => 112 ; +Readonly my $EXIT_OVERQUOTA => 113 ; +Readonly my $EXIT_ERR_APPEND => 114 ; +Readonly my $EXIT_ERR_FETCH => 115 ; +Readonly my $EXIT_ERR_CREATE => 116 ; +Readonly my $EXIT_ERR_SELECT => 117 ; +Readonly my $EXIT_TRANSFER_EXCEEDED => 118 ; + +Readonly my $EXIT_ERR_APPEND_VIRUS => 119 ; +Readonly my $EXIT_ERR_FLAGS => 120 ; + +Readonly my $EXIT_TESTS_FAILED => 254 ; # Like Test::More API + +Readonly my $EXIT_CONNECTION_FAILURE_HOST1 => 101 ; +Readonly my $EXIT_CONNECTION_FAILURE_HOST2 => 102 ; +Readonly my $EXIT_AUTHENTICATION_FAILURE_USER1 => 161 ; +Readonly my $EXIT_AUTHENTICATION_FAILURE_USER2 => 162 ; + + +Readonly my %EXIT_TXT => ( + $EX_OK => 'EX_OK: successful termination', + $EX_USAGE => 'EX_USAGE: command line usage error', + $EX_NOINPUT => 'EX_NOINPUT: cannot open input', + $EX_UNAVAILABLE => 'EX_UNAVAILABLE: service unavailable', + $EX_SOFTWARE => 'EX_SOFTWARE: internal software error', + + $EXIT_CATCH_ALL => 'EXIT_CATCH_ALL', + $EXIT_BY_SIGNAL => 'EXIT_BY_SIGNAL', + $EXIT_BY_FILE => 'EXIT_BY_FILE', + $EXIT_PID_FILE_ERROR => 'EXIT_PID_FILE_ERROR' , + $EXIT_CONNECTION_FAILURE => 'EXIT_CONNECTION_FAILURE', + $EXIT_TLS_FAILURE => 'EXIT_TLS_FAILURE', + $EXIT_AUTHENTICATION_FAILURE => 'EXIT_AUTHENTICATION_FAILURE', + $EXIT_SUBFOLDER1_NO_EXISTS => 'EXIT_SUBFOLDER1_NO_EXISTS', + $EXIT_WITH_ERRORS => 'EXIT_WITH_ERRORS', + $EXIT_WITH_ERRORS_MAX => 'EXIT_WITH_ERRORS_MAX', + $EXIT_OVERQUOTA => 'EXIT_OVERQUOTA', + $EXIT_ERR_APPEND => 'EXIT_ERR_APPEND', + $EXIT_ERR_APPEND_VIRUS => 'EXIT_ERR_APPEND_VIRUS', + $EXIT_ERR_FETCH => 'EXIT_ERR_FETCH', + $EXIT_ERR_FLAGS => 'EXIT_ERR_FLAGS', + $EXIT_ERR_CREATE => 'EXIT_ERR_CREATE', + $EXIT_ERR_SELECT => 'EXIT_ERR_SELECT', + $EXIT_TESTS_FAILED => 'EXIT_TESTS_FAILED', + $EXIT_TRANSFER_EXCEEDED => 'EXIT_TRANSFER_EXCEEDED', + $EXIT_CONNECTION_FAILURE_HOST1 => 'EXIT_CONNECTION_FAILURE_HOST1', + $EXIT_CONNECTION_FAILURE_HOST2 => 'EXIT_CONNECTION_FAILURE_HOST2', + $EXIT_AUTHENTICATION_FAILURE_USER1 => 'EXIT_AUTHENTICATION_FAILURE_USER1', + $EXIT_AUTHENTICATION_FAILURE_USER2 => 'EXIT_AUTHENTICATION_FAILURE_USER2', +) ; + + +Readonly my %EXIT_VALUE_OF_ERR_TYPE => ( + ERR_APPEND_SIZE => $EXIT_ERR_APPEND, + ERR_OVERQUOTA => $EXIT_OVERQUOTA, + ERR_APPEND => $EXIT_ERR_APPEND, + ERR_APPEND_VIRUS => $EXIT_ERR_APPEND_VIRUS, + ERR_CREATE => $EXIT_ERR_CREATE, + ERR_SELECT => $EXIT_ERR_SELECT, + ERR_Host1_FETCH => $EXIT_ERR_FETCH, + ERR_FLAGS => $EXIT_ERR_FLAGS, + ERR_UNCLASSIFIED => $EXIT_WITH_ERRORS, + ERR_NOTHING_REPORTED => $EXIT_WITH_ERRORS, + ERR_TRANSFER_EXCEEDED => $EXIT_TRANSFER_EXCEEDED, + ERR_CONNECTION_FAILURE_HOST1 => $EXIT_CONNECTION_FAILURE_HOST1, + ERR_CONNECTION_FAILURE_HOST2 => $EXIT_CONNECTION_FAILURE_HOST2, + ERR_AUTHENTICATION_FAILURE_USER1 => $EXIT_AUTHENTICATION_FAILURE_USER1, + ERR_AUTHENTICATION_FAILURE_USER2 => $EXIT_AUTHENTICATION_FAILURE_USER2, + ERR_EXIT_TLS_FAILURE => $EXIT_TLS_FAILURE, +) ; + + + +Readonly my %COMMENT_OF_ERR_TYPE => ( + ERR_APPEND_SIZE => \&comment_err_append_size, + ERR_OVERQUOTA => \&comment_err_overquota, + ERR_APPEND => \&comment_err_blank, + ERR_APPEND_VIRUS => \&comment_err_blank, + ERR_CREATE => \&comment_err_blank, + ERR_SELECT => \&comment_err_blank, + ERR_Host1_FETCH => \&comment_err_blank, + ERR_FLAGS => \&comment_err_flags, + ERR_UNCLASSIFIED => \&comment_err_blank, + ERR_NOTHING_REPORTED => \&comment_err_blank, + ERR_TRANSFER_EXCEEDED => \&comment_err_transfer_exceeded, + ERR_CONNECTION_FAILURE_HOST1 => \&comment_err_connection_failure_host1, + ERR_CONNECTION_FAILURE_HOST2 => \&comment_err_connection_failure_host2, + ERR_AUTHENTICATION_FAILURE_USER1 => \&comment_err_authentication_failure_host1, + ERR_AUTHENTICATION_FAILURE_USER2 => \&comment_err_authentication_failure_host2, + ERR_EXIT_TLS_FAILURE => \&comment_err_blank, +) ; + + +sub comment_err_blank +{ + return '' ; +} + + +sub comment_err_append_size +{ + my $mysync = shift @ARG ; + + my $comment = "The destination server refuses too big messages. Use --truncmess option. Read https://imapsync.lamiral.info/FAQ.d/FAQ.Messages_Too_Big.txt" ; + return $comment ; +} + + +sub comment_err_authentication_failure_host1 +{ + my $mysync = shift @ARG ; + + my $comment = "Check the credentials for $mysync->{ user1 }." ; + return $comment ; +} + +sub comment_err_authentication_failure_host2 +{ + my $mysync = shift @ARG ; + + my $comment = "Check the credentials for $mysync->{ user2 }." ; + return $comment ; +} + + +sub comment_err_connection_failure_host1 +{ + my $mysync = shift @ARG ; + + my $comment = "Check that host1 $mysync->{ host1 } on port $mysync->{ port1 } is the right IMAP server to be contacted for your mailbox." ; + return $comment ; +} + +sub comment_err_connection_failure_host2 +{ + my $mysync = shift @ARG ; + + my $comment = "Check that host1 $mysync->{ host2 } on port $mysync->{ port2 } is the right IMAP server to be contacted for your mailbox." ; + return $comment ; +} + +sub comment_err_overquota +{ + my $mysync = shift @ARG ; + + my $comment = 'The destination mailbox is 100% full, get free space on it and then resume the sync.' ; + return $comment ; +} + + +sub comment_err_transfer_exceeded +{ + my $mysync = shift @ARG ; + + my $size_limit_human = bytes_display_string_dec( $mysync->{ exitwhenover } ) ; + my $comment = "The maximum transfer size for a single sync is reached ( over $size_limit_human ). Relaunch the sync to sync more." ; + return $comment ; +} + +sub comment_err_flags +{ + my $mysync = shift @ARG ; + + my $comment = 'Many STORE errors with FLAGS. Retry with the option --noresyncflags' ; + return $comment ; +} + + + +Readonly my $DEFAULT_LOGDIR => 'LOG_imapsync' ; + +Readonly my $ERRORS_MAX => 50 ; # exit after 50 errors. +Readonly my $ERRORS_MAX_CGI => 20 ; # exit after 20 errors in CGI context. + + + +Readonly my $INTERVAL_TO_EXIT => 2 ; # interval max to exit instead of reconnect + +Readonly my $SPLIT => 100 ; # By default, 100 at a time, not more. +Readonly my $SPLIT_FACTOR => 10 ; # init_imap() calls Maxcommandlength( $SPLIT_FACTOR * $split ) + # which means default Maxcommandlength is 10*100 = 1000 characters ; + +Readonly my $IMAP_PORT => 143 ; # Well know port for IMAP +Readonly my $IMAP_SSL_PORT => 993 ; # Well know port for IMAP over SSL + +Readonly my $LAST => -1 ; +Readonly my $MINUS_ONE => -1 ; +Readonly my $MINUS_TWO => -2 ; + +Readonly my $RELEASE_NUMBER_EXAMPLE_1 => '1.351' ; +Readonly my $RELEASE_NUMBER_EXAMPLE_2 => 42.4242 ; + +Readonly my $TCP_PING_TIMEOUT => 5 ; +Readonly my $DEFAULT_TIMEOUT => 120 ; +Readonly my $DEFAULT_NB_RECONNECT_PER_IMAP_COMMAND => 3 ; + +Readonly my $DEFAULT_BUFFER_SIZE => 4096 ; + +Readonly my $MAX_SLEEP => 2 ; # 2 seconds max for limiting too long sleeps from --maxbytespersecond and --maxmessagespersecond + +Readonly my $DEFAULT_EXPIRATION_TIME_OAUTH2_PK12 => 3600 ; + +Readonly my $PERMISSION_FILTER => 7777 ; + +Readonly my $KIBI => 1024 ; + +Readonly my $NUMBER_10 => 10 ; +Readonly my $NUMBER_42 => 42 ; +Readonly my $NUMBER_100 => 100 ; +Readonly my $NUMBER_200 => 200 ; +Readonly my $NUMBER_300 => 300 ; +Readonly my $NUMBER_123456 => 123_456 ; +Readonly my $NUMBER_654321 => 654_321 ; + +Readonly my $NUMBER_20_000 => 20_000 ; + +Readonly my $QUOTA_PERCENT_LIMIT => 90 ; + +Readonly my $NUMBER_104_857_600 => 104_857_600 ; + +Readonly my $SIZE_MAX_STR => 64 ; + +Readonly my $NB_SECONDS_IN_A_DAY => 86_400 ; + +Readonly my $STD_CHAR_PER_LINE => 80 ; + +Readonly my $TRUE => 1 ; +Readonly my $FALSE => 0 ; + +Readonly my $LAST_RESSORT_SEPARATOR => q{/} ; + +Readonly my $CGI_TMPDIR_TOP => '/var/tmp/imapsync_cgi' ; +Readonly my $CGI_HASHFILE => '/var/tmp/imapsync_hash' ; +Readonly my $UMASK_PARANO => '0077' ; + +Readonly my $STR_use_releasecheck => q{Check if a new imapsync release is available by adding --releasecheck} ; + +Readonly my $GMAIL_MAXSIZE => 35_651_584 ; + +Readonly my $FORCE => 1 ; + +# if ( 'MSWin32' eq $OSNAME ) +# if ( 'darwin' eq $OSNAME ) +# if ( 'linux' eq $OSNAME ) + + + +# global variables +# Currently working to finish with only $sync, $acc1, $acc2 +# Not finished yet... + +my( + $sync, $acc1, $acc2, + $debugflags, + $debuglist, $debugdev, $debugmaxlinelength, $debugcgi, + @include, @exclude, @folderrec, + @folderfirst, @folderlast, + @h1_folders_all, %h1_folders_all, + @h2_folders_all, %h2_folders_all, + @h2_folders_from_1_wanted, %h2_folders_from_1_all, + %requested_folder, + $h1_folders_wanted_nb, $h1_folders_wanted_ct, + @h2_folders_not_in_1, + %h1_subscribed_folder, %h2_subscribed_folder, + %h2_folders_from_1_wanted, + %h2_folders_from_1_several, + $prefix1, $prefix2, + @regexmess, @skipmess, @pipemess, $pipemesscheck, + $syncflagsaftercopy, + $syncinternaldates, + $idatefromheader, + $minsize, $maxage, $minage, + $search, + @useheader, %useheader, + $skipsize, $allowsizemismatch, $buffersize, + $authmd5, $authmd51, $authmd52, + $subscribed, $subscribe, $subscribeall, + $help, + $nb_msg_skipped_dry_mode, + $h2_nb_msg_noheader, + $h1_bytes_processed, + $h1_nb_msg_end, $h1_bytes_end, + $h2_nb_msg_end, $h2_bytes_end, + $timestart_int, + $uid1, $uid2, + $split1, $split2, + $modulesversion, + $delete2folders, $delete2foldersonly, $delete2foldersbutnot, + $usecache, $debugcache, $cacheaftercopy, + $wholeheaderifneeded, %h1_msgs_copy_by_uid, $useuid, $h2_uidguess, + $checkmessageexists, + $messageidnodomain, + $fixInboxINBOX, + $maxlinelength, $maxlinelengthcmd, + $minmaxlinelength, + $fixcolonbug, + $create_folder_old, + $skipcrossduplicates, $debugcrossduplicates, + $disarmreadreceipts, + $mixfolders, + $fetch_hash_set, + $cgidir, + %month_abrev, + $SSL_VERIFY_POLICY, +) ; + +single_sync( $sync, $acc1, $acc2 ); + + + +sub single_sync +{ + +# main program +# global variables initialization + +# I'm currently removing all global variables except $sync $acc1 $acc2 +# passing each of them under +# $sync->{variable_name} +# or $acc1->{variable_name} +# or $acc1->{variable_name} + +# +$acc1 = {} ; +$acc2 = {} ; +$sync->{ acc1 } = $acc1 ; +$sync->{ acc2 } = $acc2 ; + +$acc1->{ Side } = 'Host1' ; +$acc2->{ Side } = 'Host2' ; +$acc1->{ N } = '1' ; +$acc2->{ N } = '2' ; + +$sync->{timestart} = time ; # Is a float because of use Time::HiRres + +$sync->{rcs} = q{$Id: imapsync,v 2.178 2022/01/12 21:28:37 gilles Exp gilles $} ; + +$sync->{ memory_consumption_at_start } = memory_consumption( ) || 0 ; + + + +my @loadavg = loadavg( ) ; + +$sync->{ cpu_number } = cpu_number( ) ; +$sync->{ loaddelay } = load_and_delay( $sync->{ cpu_number }, @loadavg ) ; +$sync->{ loaddelay } = 0 ; + +$sync->{ loadavg } = join( q{ }, $loadavg[ 0 ] ) + . " on $sync->{cpu_number} cores and " + . ram_memory_info( ) ; + + + +$sync->{ total_bytes_transferred } = 0 ; +$sync->{ total_bytes_skipped } = 0 ; +$sync->{ nb_msg_transferred } = 0 ; +$sync->{ nb_msg_skipped } = $nb_msg_skipped_dry_mode = 0 ; + +$sync->{ acc1 }->{ nb_msg_deleted } = 0 ; +$sync->{ acc2 }->{ nb_msg_deleted } = 0 ; + +$sync->{ acc1 }->{ nb_msg_duplicate } = 0 ; +$sync->{ acc2 }->{ nb_msg_duplicate } = 0 ; + +$sync->{ h1_nb_msg_noheader } = 0 ; +$h2_nb_msg_noheader = 0 ; + + +$sync->{ h1_nb_msg_start } = 0 ; +$sync->{ h1_bytes_start } = 0 ; +$sync->{ h2_nb_msg_start } = 0 ; +$sync->{ h2_bytes_start } = 0 ; +$sync->{ h1_nb_msg_processed } = $h1_bytes_processed = 0 ; + +$sync->{ h2_nb_msg_crossdup } = 0 ; + +#$h1_nb_msg_end = $h1_bytes_end = 0 ; +#$h2_nb_msg_end = $h2_bytes_end = 0 ; + +$sync->{ nb_errors } = 0; +$sync->{ biggest_message_transferred } = 0; + +%month_abrev = ( + Jan => '00', + Feb => '01', + Mar => '02', + Apr => '03', + May => '04', + Jun => '05', + Jul => '06', + Aug => '07', + Sep => '08', + Oct => '09', + Nov => '10', + Dec => '11', +); + + + +# Just create a CGI object if under cgi context only. +# Needed for the get_options() call +cgibegin( $sync ) ; + +# In cgi context, printing must start by the header so we delay other prints by using output() storage +my $options_good = get_options( $sync, @ARGV ) ; +# Is it the first myprint? +cgibuildheader( $sync ) ; +docker_context( $sync ) ; + +print_output_if_needed( $sync ) ; + + +output_reset_with( $sync ) ; + +# don't go on if options are not all known. +if ( ! defined $options_good ) { exit $EX_USAGE ; } + +# If you want releasecheck not to be done by default (like the github maintainer), +# then just uncomment the first "$sync->{releasecheck} =" line, the line ending with "0 ;", +# the second line (ending with "1 ;") can then stay active or be commented, +# the result will be the same: no releasecheck by default (because 0 is then the defined value). + +#$sync->{releasecheck} = defined $sync->{releasecheck} ? $sync->{releasecheck} : 0 ; +$sync->{releasecheck} = defined $sync->{releasecheck} ? $sync->{releasecheck} : 1 ; + +# just the version +if ( $sync->{ version } ) { + myprint( imapsync_version( $sync ), "\n" ) ; + return 0 ; +} + +#$sync->{debugenv} = 1 ; +$sync->{debugenv} and printenv( $sync ) ; # if option --debugenv +load_modules( ) ; + +# after_get_options call usage and exit if --help or options were not well got +after_get_options( $sync, $options_good ) ; + +#local $ENV{TZ} = 'GMT' if ( under_cgi_context( $sync ) and 'MSWin32' ne $OSNAME ) ; +#output( $sync, localtime(time) . " " . gmtime(time) . "\n" ) ; + +# Under CGI environment, fix caveat emptor potential issues +cgisetcontext( $sync ) ; + +get_options_extra( $sync ) ; + +# --gmail --gmail --exchange --office etc. +easyany( $sync ) ; + +$sync->{ sanitize } = defined $sync->{ sanitize } ? $sync->{ sanitize } : 1 ; +sanitize( $sync ) ; + +$sync->{ tmpdir } ||= File::Spec->tmpdir( ) ; + +# Unit tests +my $unittestssuite = unittestssuite( $sync ) ; + + +if ( condition_to_leave_after_tests( $sync ) ) +{ + return $unittestssuite ; +} + +# init live varaiables + +if ( $sync->{ testslive } ) +{ + testslive_init( $sync ) ; +} + +if ( $sync->{ testslive6 } ) +{ + testslive6_init( $sync ) ; +} + +define_pidfile( $sync ) ; +if ( $sync->{ abortbyfile } ) { $sync->{ abort } = 1 ; } + +install_signals( $sync ) ; + +$sync->{ log } = defined $sync->{ log } ? $sync->{ log } : 1 ; +$sync->{ errorsdump } = defined $sync->{ errorsdump } ? $sync->{ errorsdump } : 1 ; +$sync->{ errorsmax } = defined $sync->{ errorsmax } ? $sync->{ errorsmax } : $ERRORS_MAX ; + +# log and output +binmode STDOUT, ":encoding(UTF-8)" ; + + +if ( $sync->{ log } ) { + setlogfile( $sync ) ; + teelaunch( $sync ) ; + # now $sync->{tee} is a filehandle to STDOUT and the logfile +} + +#binmode STDERR, ":encoding(UTF-8)" ; +# STDERR goes to the same place: LOG and STDOUT (if logging is on) +# Useful only for --debugssl +$sync->{tee} and local *STDERR = *${$sync->{tee}}{IO} ; + + + +$timestart_int = int( $sync->{timestart} ) ; +$sync->{timebefore} = $sync->{timestart} ; + + +$sync->{ timestart_str } = localtimez( $sync->{timestart} ) ; + +# The prints in the log starts here + +myprint( localhost_info( $sync ), "\n" ) ; +myprint( "Transfer started at $sync->{ timestart_str }\n" ) ; +myprint( "PID is $PROCESS_ID my PPID is ", mygetppid( ), "\n" ) ; +announcelogfile( $sync ) ; +myprint( "Load is " . ( join( q{ }, loadavg( ) ) || 'unknown' ), " on $sync->{cpu_number} cores\n" ) ; +#myprintf( "Memory consumption so far: %.1f MiB\n", memory_consumption( ) / $KIBI / $KIBI ) ; +myprint( 'Current directory is ' . getcwd( ) . "\n" ) ; +myprint( 'Real user id is ' . getpwuid_any_os( $REAL_USER_ID ) . " (uid $REAL_USER_ID)\n" ) ; +myprint( 'Effective user id is ' . getpwuid_any_os( $EFFECTIVE_USER_ID ). " (euid $EFFECTIVE_USER_ID)\n" ) ; + +$modulesversion = defined $modulesversion ? $modulesversion : 1 ; + +$sync->{ warn_release } = ( $sync->{ releasecheck } ) ? check_last_release( ) : $STR_use_releasecheck ; + + +$wholeheaderifneeded = defined $wholeheaderifneeded ? $wholeheaderifneeded : 1; + +# Activate --usecache if --useuid is set and there is no --nousecache +$usecache = 1 if ( $useuid and ( ! defined $usecache ) ) ; +$cacheaftercopy = 1 if ( $usecache and ( ! defined $cacheaftercopy ) ) ; + + + + +$sync->{ checkfoldersexist } = defined $sync->{ checkfoldersexist } ? $sync->{ checkfoldersexist } : 1 ; +$checkmessageexists = defined $checkmessageexists ? $checkmessageexists : 0 ; +$sync->{ expungeaftereach } = defined $sync->{ expungeaftereach } ? $sync->{ expungeaftereach } : 1 ; + +# abletosearch is on by default +$sync->{abletosearch} = defined $sync->{abletosearch} ? $sync->{abletosearch} : 1 ; +$sync->{abletosearch1} = defined $sync->{abletosearch1} ? $sync->{abletosearch1} : $sync->{abletosearch} ; +$sync->{abletosearch2} = defined $sync->{abletosearch2} ? $sync->{abletosearch2} : $sync->{abletosearch} ; +$checkmessageexists = 0 if ( not $sync->{abletosearch1} ) ; + + +$sync->{ trylogin } = defined $sync->{ trylogin } ? $sync->{ trylogin } : 1 ; +$sync->{showpasswords} = defined $sync->{showpasswords} ? $sync->{showpasswords} : 0 ; +$sync->{ fixslash2 } = defined $sync->{ fixslash2 } ? $sync->{ fixslash2 } : 1 ; +$fixInboxINBOX = defined $fixInboxINBOX ? $fixInboxINBOX : 1 ; +$create_folder_old = defined $create_folder_old ? $create_folder_old : 0 ; +$mixfolders = defined $mixfolders ? $mixfolders : 1 ; +$sync->{automap} = defined $sync->{automap} ? $sync->{automap} : 0 ; + +$sync->{ delete2duplicates } = 1 if ( $sync->{ delete2 } and ( ! defined $sync->{ delete2duplicates } ) ) ; + +$sync->{maxmessagespersecond} = defined $sync->{maxmessagespersecond} ? $sync->{maxmessagespersecond} : 0 ; +$sync->{maxbytespersecond} = defined $sync->{maxbytespersecond} ? $sync->{maxbytespersecond} : 0 ; + +$sync->{sslcheck} = defined $sync->{sslcheck} ? $sync->{sslcheck} : 1 ; + +myprint( banner_imapsync( $sync, @ARGV ) ) ; + +myprint( "Temp directory is $sync->{ tmpdir } ( to change it use --tmpdir dirpath )\n" ) ; + +myprint( output( $sync ) ) ; +output_reset_with( $sync ) ; + +do_valid_directory( $sync->{ tmpdir } ) || croak "Error creating tmpdir $sync->{ tmpdir } : $OS_ERROR" ; + +remove_pidfile_not_running( $sync->{ pidfile } ) ; + +# if another imapsync is running then tail -f its logfile and exit +# useful in cgi context +if ( $sync->{ tail } and tail( $sync ) ) +{ + exit_clean( $sync, $EX_OK, "Tail -f finished. Now finishing myself processus $PROCESS_ID\n" ) ; + exit $EX_OK ; +} + +if ( ! write_pidfile( $sync ) ) { + myprint( "Exiting with return value $EXIT_PID_FILE_ERROR ($EXIT_TXT{$EXIT_PID_FILE_ERROR}) $sync->{nb_errors}/$sync->{errorsmax} nb_errors/max_errors PID $PROCESS_ID\n" ) ; + exit $EXIT_PID_FILE_ERROR ; +} + + +# New place for abort +# abort before simulong in order to be able to abort a simulong sync +if ( $sync->{ abort } ) +{ + abort( $sync ) ; + # well, the abort job is done, because even when not succeeded + # in aborting another run, this run has to end without doing any + # thing else + + exit $EX_OK ; +} + +# simulong is just a loop printing some lines for xx seconds with option "--simulong xx". +simulong( $sync ) ; + + + +# New place for cgiload 2019_03_03 +# because I want to log it +# Can break here if load is too heavy +# Have in mind the CGI header has already a 503 Service Unavailable +cgiload( $sync ) ; + + +$fixcolonbug = defined $fixcolonbug ? $fixcolonbug : 1 ; + +if ( $usecache and $fixcolonbug ) { tmpdir_fix_colon_bug( $sync ) } ; + +$modulesversion and myprint( "Modules version list ( use --no-modulesversion to turn off printing this Perl modules list ):\n", modulesversion(), "\n" ) ; + + +check_lib_version( $sync ) or + croak "imapsync needs perl lib Mail::IMAPClient release 3.30 or superior.\n"; + + + +if ( $sync->{ justbanner } ) +{ + myprint( "Exiting because of --justbanner\n" ) ; + exit_clean( $sync, $EX_OK ) ; +} + +# turn on RFC standard flags correction like \SEEN -> \Seen +$sync->{ flagscase } = defined $sync->{ flagscase } ? $sync->{ flagscase } : 1 ; + +# Use PERMANENTFLAGS if available +$sync->{ filterflags } = defined $sync->{ filterflags } ? $sync->{ filterflags } : 1 ; + +filterbuggyflags( $sync ) ; + + +# sync flags just after an APPEND, some servers ignore the flags given in the APPEND +# like MailEnable IMAP server. +# Off by default since it takes time. +$syncflagsaftercopy = defined $syncflagsaftercopy ? $syncflagsaftercopy : 0 ; + +# update flags on host2 for already transferred messages +$sync->{resyncflags} = defined $sync->{resyncflags} ? $sync->{resyncflags} : 1 ; +if ( $sync->{resyncflags} ) { + myprint( "Info: will resync flags for already transferred messages. Use --noresyncflags to not resync flags.\n" ) ; +}else{ + myprint( "Info: will not resync flags for already transferred messages. Use --resyncflags to resync flags.\n" ) ; +} + + +sslcheck( $sync ) ; +#print Data::Dumper->Dump( [ \$sync ] ) ; + +$split1 ||= $SPLIT ; +$split2 ||= $SPLIT ; + +#$sync->{host1} || missing_option( $sync, '--host1' ) ; +$sync->{host1} = sanitize_host( $sync->{host1} ) ; +$sync->{port1} ||= ( $sync->{ssl1} ) ? $IMAP_SSL_PORT : $IMAP_PORT ; + +#$sync->{host2} || missing_option( $sync, '--host2' ) ; +$sync->{host2} = sanitize_host( $sync->{host2} ) ; +$sync->{port2} ||= ( $sync->{ssl2} ) ? $IMAP_SSL_PORT : $IMAP_PORT ; + + +$acc1->{ debugimap } = $acc2->{ debugimap } = 1 if ( $sync->{ debugimap } ) ; +# Set on debug mode if one of the imap dialogs are in debug. +# imap dialog without the debug mode is scary and useless. +$sync->{ debug } = 1 if ( $acc1->{ debugimap } or $acc2->{ debugimap } ) ; + +# By default, don't take size to compare +$skipsize = (defined $skipsize) ? $skipsize : 1; + +$uid1 = defined $uid1 ? $uid1 : 1; +$uid2 = defined $uid2 ? $uid2 : 1; + +$subscribe = defined $subscribe ? $subscribe : 1; + +# Allow size mismatch by default +$allowsizemismatch = defined $allowsizemismatch ? $allowsizemismatch : 1; + + +if ( defined $delete2foldersbutnot or defined $delete2foldersonly ) { + $delete2folders = 1 ; +} + + +my %SSL_VERIFY_STR ; + +Readonly $SSL_VERIFY_POLICY => IO::Socket::SSL::SSL_VERIFY_NONE( ) ; +Readonly %SSL_VERIFY_STR => ( + IO::Socket::SSL::SSL_VERIFY_NONE( ) => 'SSL_VERIFY_NONE, ie, do not check the certificate server.' , + IO::Socket::SSL::SSL_VERIFY_PEER( ) => 'SSL_VERIFY_PEER, ie, check the certificate server' , +) ; + +$IO::Socket::SSL::DEBUG = defined( $sync->{debugssl} ) ? $sync->{debugssl} : 1 ; + + +if ( $sync->{ssl1} or $sync->{ssl2} or $sync->{tls1} or $sync->{tls2}) { + myprint( "SSL debug mode level is --debugssl $IO::Socket::SSL::DEBUG (can be set from 0 meaning no debug to 4 meaning max debug)\n" ) ; +} + +if ( $sync->{ssl1} ) { + myprint( qq{Host1: SSL default mode is like --sslargs1 "SSL_verify_mode=$SSL_VERIFY_POLICY", meaning for host1 $SSL_VERIFY_STR{$SSL_VERIFY_POLICY}\n} ) ; + myprint( 'Host1: Use --sslargs1 SSL_verify_mode=' . IO::Socket::SSL::SSL_VERIFY_PEER( ) . " to have $SSL_VERIFY_STR{IO::Socket::SSL::SSL_VERIFY_PEER( )} of host1\n" ) ; + # $sync->{ acc1 }->{sslargs}->{SSL_verify_mode} +} + +if ( $sync->{ssl2} ) { + myprint( qq{Host2: SSL default mode is like --sslargs2 "SSL_verify_mode=$SSL_VERIFY_POLICY", meaning for host2 $SSL_VERIFY_STR{$SSL_VERIFY_POLICY}\n} ) ; + myprint( 'Host2: Use --sslargs2 SSL_verify_mode=' . IO::Socket::SSL::SSL_VERIFY_PEER( ) . " to have $SSL_VERIFY_STR{IO::Socket::SSL::SSL_VERIFY_PEER( )} of host2\n" ) ; +} + +# ID on by default since 1.832 +$sync->{id} = defined $sync->{id} ? $sync->{id} : 1 ; + +if ( $sync->{justconnect} + or not $sync->{user1} + or not $sync->{user2} + or not $sync->{host1} + or not $sync->{host2} + ) +{ + my $justconnect = justconnect( $sync ) ; + + myprint( debugmemory( $sync, " after justconnect() call" ) ) ; + exit_clean( $sync, $EX_OK, + "Exiting after a justconnect on host(s): $justconnect\n" + ) ; +} + + +#$sync->{user1} || missing_option( $sync, '--user1' ) ; +#$sync->{user2} || missing_option( $sync, '--user2' ) ; + +$syncinternaldates = defined $syncinternaldates ? $syncinternaldates : 1; + +# Turn on expunge if there is not explicit option --noexpunge1 and option +# --delete1 is given. +# Done because --delete1 --noexpunge1 is very dangerous on the second run: +# the Deleted flag is then synced to all previously transferred messages. +# So --delete1 implies --expunge1 is a better usability default behavior. +if ( $sync->{ delete1 } ) { + if ( ! defined $sync->{ expunge1 } ) { + myprint( "Info: turning on --expunge1 because --delete1 --noexpunge1 is very dangerous on the second run.\n" ) ; + $sync->{ expunge1 } = 1 ; + } + myprint( "Info: if expunging after each message slows down too much the sync then use --noexpungeaftereach to speed up\n" ) ; +} + +if ( $sync->{ uidexpunge2 } and not Mail::IMAPClient->can( 'uidexpunge' ) ) { + myprint( "Failure: uidexpunge not supported (IMAPClient release < 3.17), use nothing or --expunge2 instead\n" ) ; + $sync->{nb_errors}++ ; + exit_clean( $sync, $EX_SOFTWARE ) ; +} + +if ( ( $sync->{ delete2 } or $sync->{ delete2duplicates } ) and not defined $sync->{ uidexpunge2 } ) { + if ( Mail::IMAPClient->can( 'uidexpunge' ) ) { + myprint( "Info: will act as --uidexpunge2\n" ) ; + $sync->{ uidexpunge2 } = 1 ; + }elsif ( not defined $sync->{ expunge2 } ) { + myprint( "Info: will act as --expunge2 (no uidexpunge support)\n" ) ; + $sync->{ expunge2 } = 1 ; + } +} + +if ( $sync->{ delete1 } and $sync->{ delete2 } ) { + myprint( "Warning: using --delete1 and --delete2 together is almost always a bad idea. " + . "You should probably launch two runs, the first with --delete2 for a strict sync, " + . "then the second with --delete1 to remove messages from the source account. " + . "Exiting imapsync.\n" ) ; + $sync->{ nb_errors }++ ; + exit_clean( $sync, $EX_USAGE ) ; +} + +if ( $idatefromheader ) { + myprint( 'Turned ON idatefromheader, ', + "will set the internal dates on host2 from the 'Date:' header line.\n" ) ; + $syncinternaldates = 0 ; +} + +if ( $syncinternaldates ) { + myprint( 'Info: turned ON syncinternaldates, ', + "will set the internal dates (arrival dates) on host2 same as host1.\n" ) ; +}else{ + myprint( "Info: turned OFF syncinternaldates\n" ) ; +} + +if ( defined $authmd5 and $authmd5 ) { + $authmd51 = 1 ; + $authmd52 = 1 ; +} + +if ( defined $authmd51 and $authmd51 ) { + $acc1->{ authmech } ||= 'CRAM-MD5' ; +} +else{ + $acc1->{ authmech } ||= $acc1->{ authuser } ? 'PLAIN' : 'LOGIN' ; +} + +if ( defined $authmd52 and $authmd52 ) { + $acc2->{ authmech } ||= 'CRAM-MD5'; +} +else{ + $acc2->{ authmech } ||= $acc2->{ authuser } ? 'PLAIN' : 'LOGIN'; +} + +$acc1->{ authmech } = uc $acc1->{ authmech } ; +$acc2->{ authmech } = uc $acc2->{ authmech } ; + +if ( defined $acc1->{ proxyauth } && !$acc1->{ authuser } ) +{ + missing_option( $sync, 'With --proxyauth1, --authuser1' ) ; +} + +if ( defined $acc2->{ proxyauth } && !$acc2->{ authuser } ) +{ + missing_option( $sync, 'With --proxyauth2, --authuser2' ) ; +} + +myprint( "Host1: will try to use $acc1->{ authmech } authentication on host1\n") ; +myprint( "Host2: will try to use $acc2->{ authmech } authentication on host2\n") ; + +$sync->{ timeout } = defined $sync->{ timeout } ?$sync->{ timeout } : $DEFAULT_TIMEOUT ; + +$sync->{ acc1 }->{timeout} = defined $sync->{ acc1 }->{timeout} ? $sync->{ acc1 }->{timeout} : $sync->{ timeout } ; +myprint( "Host1: imap connection timeout is $sync->{ acc1 }->{timeout} seconds\n") ; +$sync->{ acc2 }->{timeout} = defined $sync->{ acc2 }->{timeout} ? $sync->{ acc2 }->{timeout} : $sync->{ timeout } ; +myprint( "Host2: imap connection timeout is $sync->{ acc2 }->{timeout} seconds\n" ) ; + + +keepalive1( $sync ) ; +keepalive2( $sync ) ; + + +if ( under_cgi_context( $sync ) ) +{ + myprint( "Under CGI context, a timeout can occur from the webserver, see https://imapsync.lamiral.info/INSTALL.d/INSTALL.OnlineUI.txt\n" ) ; +} + +$sync->{ syncacls } = defined $sync->{ syncacls } ? $sync->{ syncacls } : 0 ; + +# No folders sizes at the beginning if --justfolders, unless really wanted. +if ( + $sync->{ justfolders } + and not defined $sync->{ foldersizes } + and not $sync->{ justfoldersizes } ) +{ + $sync->{ foldersizes } = 0 ; + $sync->{ foldersizesatend } = 1 ; +} + +$sync->{ foldersizes } = ( defined $sync->{ foldersizes } ) ? $sync->{ foldersizes } : 1 ; +$sync->{ foldersizesatend } = ( defined $sync->{ foldersizesatend } ) ? $sync->{ foldersizesatend } : $sync->{ foldersizes } ; + +#$sync->{ checknoabletosearch } = ( defined $sync->{ checknoabletosearch } ) ? $sync->{ checknoabletosearch } : 1 ; +set_checknoabletosearch( $sync ) ; + + +$acc1->{ fastio } = defined $acc1->{ fastio } ? $acc1->{ fastio } : 0 ; +$acc2->{ fastio } = defined $acc2->{ fastio } ? $acc2->{ fastio } : 0 ; + + +$acc1->{ reconnectretry } = defined $acc1->{ reconnectretry } ? $acc1->{ reconnectretry } : $DEFAULT_NB_RECONNECT_PER_IMAP_COMMAND ; +$acc2->{ reconnectretry } = defined $acc2->{ reconnectretry } ? $acc2->{ reconnectretry } : $DEFAULT_NB_RECONNECT_PER_IMAP_COMMAND ; + +# IMAP compression on by default +#$acc1->{ compress } = defined $acc1->{ compress } ? $acc1->{ compress } : 0 ; +#$acc2->{ compress } = defined $acc2->{ compress } ? $acc2->{ compress } : 0 ; + + + +if ( ! @useheader ) { @useheader = qw( Message-Id Received ) ; } + +# Make a hash %useheader of each --useheader 'key' in uppercase +for ( @useheader ) { $sync->{useheader}->{ uc $_ } = undef } ; + +#myprint( Data::Dumper->Dump( [ \%useheader ] ) ) ; +#exit ; + +myprint( "Host1: IMAP server [$sync->{host1}] port [$sync->{port1}] user [$sync->{user1}]\n" ) ; +myprint( "Host2: IMAP server [$sync->{host2}] port [$sync->{port2}] user [$sync->{user2}]\n" ) ; + +get_password1( $sync ) ; +get_password2( $sync ) ; + +# --dry1 make imapsync not fetching messages from host1, it is on when --dry is on. +# Use --dry --nodry1 to make imapsync fetching messages from host1, +# It is useful when debugging transformation options like --pipemess or --regexmess +$sync->{dry1} = defined $sync->{dry1} ? $sync->{dry1} : $sync->{dry} ; + +$sync->{dry_message} = q{} ; +if( $sync->{dry} ) { + $sync->{dry_message} = "\t(not really since --dry mode)" ; +} + +$sync->{ search1 } ||= $search if ( $search ) ; +$sync->{ search2 } ||= $search if ( $search ) ; + +if ( $disarmreadreceipts ) +{ + push @regexmess, q{s{\A((?:[^\n]+\r\n)+|)(^Disposition-Notification-To:[^\n]*\n)(\r?\n|.*\n\r?\n)}{$1X-$2$3}ims} ; +} + +$pipemesscheck = ( defined $pipemesscheck ) ? $pipemesscheck : 1 ; + +if ( @pipemess and $pipemesscheck ) { + myprint( 'Checking each --pipemess command, ' + . join( q{, }, @pipemess ) + . ", with an space string. ( Can avoid this check with --nopipemesscheck )\n" ) ; + my $string = pipemess( q{ }, @pipemess ) ; + # string undef means something was bad. + if ( not ( defined $string ) ) { + $sync->{nb_errors}++ ; + exit_clean( $sync, $EX_USAGE, + "Error: one of --pipemess command is bad, check it\n" + ) ; + } + myprint( "Ok with each --pipemess @pipemess\n" ) ; +} + +if ( $maxlinelengthcmd ) { + myprint( "Checking --maxlinelengthcmd command, + $maxlinelengthcmd, with an space string.\n" + ) ; + my $string = pipemess( q{ }, $maxlinelengthcmd ) ; + # string undef means something was bad. + if ( not ( defined $string ) ) { + $sync->{nb_errors}++ ; + exit_clean( $sync, $EX_USAGE, + "Error: --maxlinelengthcmd command is bad, check it\n" + ) ; + } + myprint( "Ok with --maxlinelengthcmd $maxlinelengthcmd\n" ) ; +} + +if ( @regexmess ) { + my $string = regexmess( q{ } ) ; + myprint( "Checking each --regexmess command with an space string.\n" ) ; + # string undef means one of the eval regex was bad. + if ( not ( defined $string ) ) { + #errors_incr( $sync, 'Warning: one of --regexmess option may be bad, check them' ) ; + exit_clean( $sync, $EX_USAGE, + "Error: one of --regexmess option is bad, check it\n" + ) ; + } + myprint( "Ok with each --regexmess\n" ) ; +} + +if ( @skipmess ) { + myprint( "Checking each --skipmess command with an space string.\n" ) ; + my $match = skipmess( q{ } ) ; + # match undef means one of the eval regex was bad. + if ( not ( defined $match ) ) { + $sync->{nb_errors}++ ; + exit_clean( $sync, $EX_USAGE, + "Error: one of --skipmess option is bad, check it\n" + ) ; + } + myprint( "Ok with each --skipmess\n" ) ; +} + +if ( $sync->{ regexflag } ) { + myprint( "Checking each --regexflag command with an space string.\n" ) ; + my $string = regexflags( $sync, q{ } ) ; + # string undef means one of the eval regex was bad. + if ( not ( defined $string ) ) { + $sync->{nb_errors}++ ; + exit_clean( $sync, $EX_USAGE, + "Error: one of --regexflag option is bad, check it\n" + ) ; + } + myprint( "Ok with each --regexflag\n" ) ; +} + +$sync->{imap1} = login_imap( $sync->{host1}, $sync->{port1}, $sync->{user1}, $sync->{password1}, + $sync->{ssl1}, $sync->{tls1}, + $uid1, $split1, $sync->{ acc1 }, $sync ) ; + +$sync->{imap2} = login_imap( $sync->{host2}, $sync->{port2}, $sync->{user2}, $sync->{password2}, + $sync->{ssl2}, $sync->{tls2}, + $uid2, $split2, $sync->{ acc2 }, $sync ) ; + + +$sync->{ debug } and $sync->{imap1} and myprint( 'Host1 Buffer I/O: ', $sync->{imap1}->Buffer(), "\n" ) ; +$sync->{ debug } and $sync->{imap2} and myprint( 'Host2 Buffer I/O: ', $sync->{imap2}->Buffer(), "\n" ) ; + + +if ( ! $sync->{imap1} || ! $sync->{imap2} ) +{ + exit_most_errors( $sync ) ; +} + + +myprint( "Host1: state Authenticated\n" ) ; +myprint( "Host2: state Authenticated\n" ) ; + +myprint( 'Host1 capability once authenticated: ', join(q{ }, @{ $sync->{imap1}->capability() || [] }), "\n" ) ; + +#myprint( Data::Dumper->Dump( [ $sync->{imap1} ] ) ) ; +#myprint( "imap4rev1: " . $sync->{imap1}->imap4rev1() . "\n" ) ; + +myprint( 'Host2 capability once authenticated: ', join(q{ }, @{ $sync->{imap2}->capability() || [] }), "\n" ) ; + +imap_id_stuff( $sync ) ; + +#quota( $sync, $sync->{imap1}, 'h1' ) ; # quota on host1 is useless and pollute host2 output. +quota( $sync, $sync->{imap2}, 'h2' ) ; + +maxsize_setting( $sync ) ; + +acc_compress_imap( $acc1 ) ; +acc_compress_imap( $acc2 ) ; + +if ( $sync->{ justlogin } ) { + $sync->{imap1}->logout( ) ; + $sync->{imap2}->logout( ) ; + exit_clean( $sync, $EX_OK, "Exiting because of --justlogin\n" ) ; +} + + +# +# Folder stuff +# + +$h1_folders_wanted_nb = 0 ; # counter of folders to be done. +$h1_folders_wanted_ct = 0 ; # counter of folders done. + +# All folders on host1 and host2 + +@h1_folders_all = sort $sync->{imap1}->folders( ) ; +@h2_folders_all = sort $sync->{imap2}->folders( ) ; + +myprint( 'Host1: found ', scalar @h1_folders_all , " folders.\n" ) ; +myprint( 'Host2: found ', scalar @h2_folders_all , " folders.\n" ) ; + +foreach my $f ( @h1_folders_all ) +{ + $h1_folders_all{ $f } = 1 +} + +foreach my $f ( @h2_folders_all ) +{ + $h2_folders_all{ $f } = 1 ; + $sync->{h2_folders_all_UPPER}{ uc $f } = 1 ; +} + +$sync->{h1_folders_all} = \%h1_folders_all ; +$sync->{h2_folders_all} = \%h2_folders_all ; + + +private_folders_separators_and_prefixes( ) ; + + +# Make a hash of subscribed folders in both servers. + +for ( $sync->{imap1}->subscribed( ) ) { $h1_subscribed_folder{ $_ } = 1 } ; +for ( $sync->{imap2}->subscribed( ) ) { $h2_subscribed_folder{ $_ } = 1 } ; + + +if ( defined $sync->{ subfolder1 } ) { + subfolder1( $sync ) ; +} + + + + +if ( defined $sync->{ subfolder2 } ) { + subfolder2( $sync ) ; +} + +if ( $fixInboxINBOX and ( my $reg = fix_Inbox_INBOX_mapping( \%h1_folders_all, \%h2_folders_all ) ) ) { + push @{ $sync->{ regextrans2 } }, $reg ; +} + + + +if ( ( $sync->{ folder } and scalar @{ $sync->{ folder } } ) + or $subscribed + or scalar @folderrec ) +{ + # folders given by option --folder + if ( $sync->{ folder } and scalar @{ $sync->{ folder } } ) { + add_to_requested_folders( @{ $sync->{ folder } } ) ; + } + + # option --subscribed + if ( $subscribed ) { + add_to_requested_folders( keys %h1_subscribed_folder ) ; + } + + # option --folderrec + if ( scalar @folderrec ) { + foreach my $folderrec ( @folderrec ) { + add_to_requested_folders( $sync->{imap1}->folders( $folderrec ) ) ; + } + } +} +else +{ + # no include, no folder/subscribed/folderrec options => all folders + if ( not scalar @include ) { + myprint( "Including all folders found by default. Use --subscribed or --folder or --folderrec or --include to select specific folders. Use --exclude to unselect specific folders.\n" ) ; + add_to_requested_folders( @h1_folders_all ) ; + } +} + + +# consider (optional) includes and excludes +if ( scalar @include ) { + foreach my $include ( @include ) { + # No, do not add /x after the regex, never. + # Users would kill you! + my @included_folders = grep { /$include/ } @h1_folders_all ; + add_to_requested_folders( @included_folders ) ; + myprint( "Including folders matching pattern $include\n" . jux_utf8_list( @included_folders ) . "\n" ) ; + } +} + +if ( scalar @exclude ) { + foreach my $exclude ( @exclude ) { + my @requested_folder = sort keys %requested_folder ; + # No, do not add /x after the regex, never. + # Users would kill you! + my @excluded_folders = grep { /$exclude/ } @requested_folder ; + remove_from_requested_folders( @excluded_folders ) ; + myprint( "Excluding folders matching pattern $exclude\n" . jux_utf8_list( @excluded_folders ) . "\n" ) ; + } +} + + +# sort before is not very powerful +# it adds --folderfirst and --folderlast even if they don't exist on host1 +#@h1_folders_wanted = sort_requested_folders( ) ; +$sync->{h1_folders_wanted} = [ sort_requested_folders( ) ] ; + +# Remove no selectable folders + + +if ( $sync->{ checkfoldersexist } ) { + my @h1_folders_wanted_exist ; + myprint( "Host1: Checking wanted folders exist. Use --nocheckfoldersexist to avoid this check (shared of public namespace targeted).\n" ) ; + foreach my $folder ( @{ $sync->{h1_folders_wanted} } ) { + ( $sync->{ debug } or $sync->{debugfolders} ) and myprint( "Checking $folder exists on host1\n" ) ; + if ( ! exists $h1_folders_all{ $folder } ) { + myprint( "Host1: warning! ignoring folder $folder because it is not in host1 whole folders list.\n" ) ; + next ; + }else{ + push @h1_folders_wanted_exist, $folder ; + } + } + @{ $sync->{h1_folders_wanted} } = @h1_folders_wanted_exist ; +}else{ + myprint( "Host1: Not checking that wanted folders exist. Remove --nocheckfoldersexist to get this check.\n" ) ; +} + +setcheckselectable( $sync ) ; + +checkselectable( $sync ) ; + + + +# Bugfix OpenFind folders named like "kk \*123" are in fact "kk *123" (no \) +#foreach my $folder ( @{ $sync->{ h1_folders_wanted } } ) +#{ +# $folder =~ s{ \\\*}{ *}g ; +#} + + +# this hack is because LWP post does not pass well a hash in the $form parameter +# but it does pass well an array +%{ $sync->{f1f2h} } = split_around_equal( @{ $sync->{f1f2} } ) ; + +automap( $sync ) ; + + +foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) { + my $h2_fold ; + $h2_fold = imap2_folder_name( $sync, $h1_fold ) ; + $h2_folders_from_1_wanted{ $h2_fold }++ ; + if ( 1 < $h2_folders_from_1_wanted{ $h2_fold } ) { + $h2_folders_from_1_several{ $h2_fold }++ ; + } +} + +@h2_folders_from_1_wanted = sort keys %h2_folders_from_1_wanted; + + +foreach my $h1_fold ( @h1_folders_all ) { + my $h2_fold ; + $h2_fold = imap2_folder_name( $sync, $h1_fold ) ; + $h2_folders_from_1_all{ $h2_fold }++ ; + # Follows a fix to avoid deleting folder $sync->{ subfolder2 } + # because it usually does not exist on host1. + if ( $sync->{ subfolder2 } ) + { + $h2_folders_from_1_all{ $sync->{ h2_prefix } . $sync->{ subfolder2 } }++ ; + $h2_folders_from_1_all{ $sync->{ subfolder2 } }++ ; + } +} + + + +myprint( << 'END_LISTING' ) ; + +++++ Listing folders +All foldernames are presented between brackets like [X] where X is the foldername. +When a foldername contains non-ASCII characters it is presented in the form +[X] = [Y] where +X is the imap foldername you have to use in command line options and +Y is the utf8 output just printed for convenience, to recognize it. + +END_LISTING + +myprint( + "Host1: folders list (first the raw imap format then the [X] = [Y]):\n", + $sync->{imap1}->list( ), + "\n", + jux_utf8_list( @h1_folders_all ), + "\n", + "Host2: folders list (first the raw imap format then the [X] = [Y]):\n", + $sync->{imap2}->list( ), + "\n", + jux_utf8_list( @h2_folders_all ), + "\n", + q{} +) ; + +if ( $subscribed ) { + myprint( + 'Host1 subscribed folders list: ', + jux_utf8_list( sort keys %h1_subscribed_folder ), "\n", + ) ; +} + + + +@h2_folders_not_in_1 = list_folders_in_2_not_in_1( ) ; + +if ( @h2_folders_not_in_1 ) { + myprint( "Folders in host2 not in host1:\n", + jux_utf8_list( @h2_folders_not_in_1 ), "\n" ) ; +} + + +if ( keys %{ $sync->{f1f2auto} } ) { + myprint( "Folders mapping from --automap feature (use --f1f2 to override any mapping):\n" ) ; + foreach my $h1_fold ( keys %{ $sync->{f1f2auto} } ) { + my $h2_fold = $sync->{f1f2auto}{$h1_fold} ; + myprintf( "%-40s -> %-40s\n", + jux_utf8( $h1_fold ), jux_utf8( $h2_fold ) ) ; + } + myprint( "\n" ) ; +} + +if ( keys %{ $sync->{f1f2h} } ) { + myprint( "Folders mapping from --f1f2 options, it overrides --automap:\n" ) ; + foreach my $h1_fold ( keys %{ $sync->{f1f2h} } ) { + my $h2_fold = $sync->{f1f2h}{$h1_fold} ; + my $warn = q{} ; + if ( not exists $h1_folders_all{ $h1_fold } ) { + $warn = "BUT $h1_fold does NOT exist on host1!" ; + } + myprintf( "%-40s -> %-40s %s\n", + jux_utf8( $h1_fold ), jux_utf8( $h2_fold ), $warn ) ; + } + myprint( "\n" ) ; +} + +exit_clean( $sync, $EX_OK, "Exiting because of --justfolderlists\n" ) if ( $sync->{ justfolderlists } ) ; +exit_clean( $sync, $EX_OK, "Exiting because of --justautomap\n" ) if ( $sync->{ justautomap } ) ; + +debugsleep( $sync ) ; + +if ( $sync->{ skipemptyfolders } ) +{ + myprint( "Host1: will not syncing empty folders on host1. Use --noskipemptyfolders to create them anyway on host2\n") ; +} + +if ( $sync->{ checknoabletosearch } ) +{ + myprint( "Checking SEARCH ALL works on both accounts. To avoid that check, use --nochecknoabletosearch\n" ) ; + my $check1 = checknoabletosearch( $sync, $sync->{ imap1 }, 'INBOX', 'Host1' ) ; + my $check2 = checknoabletosearch( $sync, $sync->{ imap2 }, 'INBOX', 'Host2' ) ; + if ( $check1 or $check2 ) + { + myprint( "At least one account can not SEARCH ALL. So acting like --noabletosearch\n" ) ; + $sync->{abletosearch} = 0 ; + $sync->{abletosearch1} = 0 ; + $sync->{abletosearch2} = 0 ; + } + else + { + myprint( "Good! SEARCH ALL works on both accounts.\n" ) ; + } +} + + + +if ( $sync->{ foldersizes } ) { + + foldersizes_at_the_beggining( $sync ) ; + #foldersizes_at_the_beggining_old( $sync ) ; +} + + + +if ( $sync->{ justfoldersizes } ) +{ + exit_clean( $sync, $EX_OK, "Exiting because of --justfoldersizes\n" ) ; +} + +$sync->{can_do_stats} = 1 ; + +if ( $sync->{ delete1emptyfolders } ) { + delete1emptyfolders( $sync ) ; +} + +delete_folders_in_2_not_in_1( ) if $delete2folders ; + +# folder loop +$h1_folders_wanted_nb = scalar @{ $sync->{h1_folders_wanted} } ; + +myprint( "++++ Looping on each one of $h1_folders_wanted_nb folders to sync\n" ) ; + +$sync->{begin_transfer_time} = time ; + +my %uid_candidate_for_deletion ; +my %uid_candidate_no_deletion ; + +$sync->{ h2_folders_of_md5 } = { } ; + + +FOLDER: foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) +{ + $sync->{ h1_current_folder } = $h1_fold ; + eta_print( $sync ) ; + abortifneeded( $sync ) ; + if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } + + my $h2_fold = imap2_folder_name( $sync, $h1_fold ) ; + $sync->{ h2_current_folder } = $h2_fold ; + + $h1_folders_wanted_ct++ ; + myprintf( "Folder %7s %-35s -> %-35s\n", "$h1_folders_wanted_ct/$h1_folders_wanted_nb", + jux_utf8( $h1_fold ), jux_utf8( $h2_fold ) ) ; + myprint( debugmemory( $sync, " at folder loop" ) ) ; + + # host1 can not be fetched read only, select is needed because of expunge. + select_folder( $sync, $sync->{imap1}, $h1_fold, 'Host1' ) or next FOLDER ; + + debugsleep( $sync ) ; + + my $h1_msgs_all_hash_ref ; + my @h1_msgs ; + my $h1_msgs_nb ; + my $h1_msgs_nb_from_select ; + + $h1_msgs_nb_from_select = count_from_select( $sync->{imap1}->History ) ; + myprint( "Host1: folder [$h1_fold] has $h1_msgs_nb_from_select messages in total (mentioned by SELECT)\n" ) ; + + if ( $sync->{ skipemptyfolders } and 0 == $h1_msgs_nb_from_select ) { + myprint( "Host1: skipping empty host1 folder [$h1_fold]\n" ) ; + next FOLDER ; + } + + # Code added from https://github.com/imapsync/imapsync/issues/95 + # Thanks jh1995 + # Goal: do not create folder if --search or --max/minage return 0 message. + # even if there are messages by SELECT (no not real empty, empty for the user point of vue). + if ( $sync->{ skipemptyfolders } or $sync->{ dry } ) + { + $h1_msgs_all_hash_ref = { } ; + @h1_msgs = select_msgs( $sync->{imap1}, $h1_msgs_all_hash_ref, $sync->{ search1 }, $sync->{abletosearch1}, $h1_fold ) ; + + $h1_msgs_nb = scalar( @h1_msgs ) ; + if ( 0 == $h1_msgs_nb and $sync->{ skipemptyfolders } ) { + myprint( "Host1: skipping empty host1 folder [$h1_fold] (0 message found by SEARCH)\n" ) ; + next FOLDER ; + } + } + + if ( ! exists $h2_folders_all{ $h2_fold } ) { + # In --dry mode I could count the messages to be transfered instead of 0 + # Messages transferred : 0 (could be 0 without dry mode) + if ( ! create_folder( $sync, $sync->{imap2}, $h2_fold, $h1_fold ) ) + { + if ( $sync->{ dry } ) + { + $nb_msg_skipped_dry_mode += $h1_msgs_nb ; + } + next FOLDER ; + } + } + + acls_sync( $sync, $h1_fold, $h2_fold ) ; + + # Sometimes the folder on host2 is listed (it exists) but is + # not selectable but becomes selectable by a create (Gmail) + select_folder( $sync, $sync->{imap2}, $h2_fold, 'Host2' ) + or ( create_folder( $sync, $sync->{imap2}, $h2_fold, $h1_fold ) + and select_folder( $sync, $sync->{imap2}, $h2_fold, 'Host2' ) ) + or next FOLDER ; + my @select_results = $sync->{imap2}->Results( ) ; + + my $h2_fold_nb_messages = count_from_select( @select_results ) ; + myprint( "Host2: folder [$h2_fold] has $h2_fold_nb_messages messages in total (mentioned by SELECT)\n" ) ; + + my $permanentflags2 = permanentflags( $sync, @select_results ) ; + myprint( "Host2: folder [$h2_fold] permanentflags: $permanentflags2\n" ) ; + + if ( $sync->{ expunge1 } ) + { + myprint( "Host1: Expunging $h1_fold $sync->{dry_message}\n" ) ; + if ( ! $sync->{dry} ) + { + $sync->{imap1}->expunge( ) ; + } + } + + if ( ( ( $subscribe and exists $h1_subscribed_folder{ $h1_fold } ) or $subscribeall ) + and not exists $h2_subscribed_folder{ $h2_fold } ) + { + myprint( "Host2: Subscribing to folder $h2_fold\n" ) ; + if ( ! $sync->{dry} ) { $sync->{imap2}->subscribe( $h2_fold ) } ; + } + + next FOLDER if ( $sync->{ justfolders } ) ; + + if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } + + + if ( ! defined $h1_msgs_nb ) + { + $h1_msgs_all_hash_ref = { } ; + @h1_msgs = select_msgs( $sync->{imap1}, $h1_msgs_all_hash_ref, $sync->{ search1 }, $sync->{abletosearch1}, $h1_fold ); + $h1_msgs_nb = scalar @h1_msgs ; + }else{ + # select_msgs already done. + } + + if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } + + myprint( "Host1: folder [$h1_fold] considering $h1_msgs_nb messages\n" ) ; + ( $sync->{ debug } or $debuglist ) and myprint( "Host1: folder [$h1_fold] considering $h1_msgs_nb messages, LIST gives: @h1_msgs\n" ) ; + $sync->{ debug } and myprint( "Host1: selecting messages of folder [$h1_fold] took ", timenext( $sync ), " s\n" ) ; + + my $h2_msgs_all_hash_ref = { } ; + my @h2_msgs = select_msgs( $sync->{imap2}, $h2_msgs_all_hash_ref, $sync->{ search2 }, $sync->{abletosearch2}, $h2_fold ) ; + + if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } + + my $h2_msgs_nb = scalar @h2_msgs ; + + myprint( "Host2: folder [$h2_fold] considering $h2_msgs_nb messages\n" ) ; + ( $sync->{ debug } or $debuglist ) and myprint( "Host2: folder [$h2_fold] considering $h2_msgs_nb messages, LIST gives: @h2_msgs\n" ) ; + $sync->{ debug } and myprint( "Host2: selecting messages of folder [$h2_fold] took ", timenext( $sync ), " s\n" ) ; + + my $cache_base = "$sync->{ tmpdir }/imapsync_cache/" ; + my $cache_dir = cache_folder( $cache_base, + "$sync->{host1}/$sync->{user1}/$sync->{host2}/$sync->{user2}", $h1_fold, $h2_fold ) ; + my ( $cache_1_2_ref, $cache_2_1_ref ) = ( {}, {} ) ; + + my $h1_uidvalidity = $sync->{imap1}->uidvalidity( ) || q{} ; + my $h2_uidvalidity = $sync->{imap2}->uidvalidity( ) || q{} ; + + if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } + + if ( $usecache ) { + myprint( "Local cache directory: $cache_dir ( " . length( $cache_dir ) . " characters long )\n" ) ; + mkpath( "$cache_dir" ) ; + ( $cache_1_2_ref, $cache_2_1_ref ) + = get_cache( $cache_dir, \@h1_msgs, \@h2_msgs, $h1_msgs_all_hash_ref, $h2_msgs_all_hash_ref ) ; + myprint( 'CACHE h1 h2: ', scalar keys %{ $cache_1_2_ref } , " files\n" ) ; + $sync->{ debug } and myprint( '[', + map ( { "$_->$cache_1_2_ref->{$_} " } keys %{ $cache_1_2_ref } ), " ]\n" ) ; + } + + my %h1_hash = ( ) ; + my %h2_hash = ( ) ; + + my ( %h1_msgs, %h2_msgs ) ; + @h1_msgs{ @h1_msgs } = ( ) ; + @h2_msgs{ @h2_msgs } = ( ) ; + + my @h1_msgs_in_cache = sort { $a <=> $b } keys %{ $cache_1_2_ref } ; + my @h2_msgs_in_cache = sort { $a <=> $b } keys %{ $cache_2_1_ref } ; + + my ( %h1_msgs_not_in_cache, %h2_msgs_not_in_cache ) ; + %h1_msgs_not_in_cache = %h1_msgs ; + %h2_msgs_not_in_cache = %h2_msgs ; + delete @h1_msgs_not_in_cache{ @h1_msgs_in_cache } ; + delete @h2_msgs_not_in_cache{ @h2_msgs_in_cache } ; + + my @h1_msgs_not_in_cache = sort { $a <=> $b } keys %h1_msgs_not_in_cache ; + #myprint( "h1_msgs_not_in_cache: [@h1_msgs_not_in_cache]\n" ) ; + my @h2_msgs_not_in_cache = sort { $a <=> $b } keys %h2_msgs_not_in_cache ; + + my @h2_msgs_delete2_not_in_cache = () ; + %h1_msgs_copy_by_uid = ( ) ; + + if ( $useuid ) { + # use uid so we have to avoid getting header + @h1_msgs_copy_by_uid{ @h1_msgs_not_in_cache } = ( ) ; + @h2_msgs_delete2_not_in_cache = @h2_msgs_not_in_cache if $usecache ; + @h1_msgs_not_in_cache = ( ) ; + @h2_msgs_not_in_cache = ( ) ; + + #myprint( "delete2: @h2_msgs_delete2_not_in_cache\n" ) ; + } + + $sync->{ debug } and myprint( "Host1: parsing headers of folder [$h1_fold]\n" ) ; + + my ($h1_heads_ref, $h1_fir_ref) = ({}, {}); + $h1_heads_ref = $sync->{imap1}->parse_headers([@h1_msgs_not_in_cache], @useheader) if (@h1_msgs_not_in_cache); + $sync->{ debug } and myprint( "Host1: parsing headers of folder [$h1_fold] took ", timenext( $sync ), " s\n" ) ; + + @{ $h1_fir_ref }{@h1_msgs} = ( undef ) ; + + $sync->{ debug } and myprint( "Host1: getting flags idate and sizes of folder [$h1_fold]\n" ) ; + + my @h1_common_fetch_param = ( 'FLAGS', 'INTERNALDATE', 'RFC822.SIZE' ) ; + if ( $sync->{ synclabels } or $sync->{ resynclabels } ) { push @h1_common_fetch_param, 'X-GM-LABELS' ; } + + if ( $sync->{abletosearch1} ) + { + $h1_fir_ref = $sync->{imap1}->fetch_hash( \@h1_msgs, @h1_common_fetch_param, $h1_fir_ref ) + if ( @h1_msgs ) ; + } + else + { + my $fetch_hash_uids = $fetch_hash_set || "1:*" ; + $h1_fir_ref = $sync->{imap1}->fetch_hash( $fetch_hash_uids, @h1_common_fetch_param, $h1_fir_ref ) + if ( @h1_msgs ) ; + } + + $sync->{ debug } and myprint( "Host1: getting flags idate and sizes of folder [$h1_fold] took ", timenext( $sync ), " s\n" ) ; + if ( ! $h1_fir_ref ) + { + my $error = join( q{}, "Host1: folder $h1_fold : Could not fetch_hash ", + scalar @h1_msgs, ' msgs: ', $sync->{imap1}->LastError || q{}, "\n" ) ; + errors_incr( $sync, $error ) ; + next FOLDER ; + } + + my @h1_msgs_duplicate; + foreach my $m ( @h1_msgs_not_in_cache ) + { + my $rc = parse_header_msg( $sync, $sync->{imap1}, $m, $h1_heads_ref, $h1_fir_ref, 'Host1', \%h1_hash ) ; + if ( ! defined $rc ) + { + my $h1_size = $h1_fir_ref->{$m}->{'RFC822.SIZE'} || 0; + myprint( "Host1: $h1_fold/$m size $h1_size ignored (no wanted headers so we ignore this message. To solve this: use --addheader)\n" ) ; + $sync->{ total_bytes_skipped } += $h1_size ; + $sync->{ nb_msg_skipped } += 1 ; + $sync->{ h1_nb_msg_noheader } +=1 ; + $sync->{ h1_nb_msg_processed } +=1 ; + } elsif( 0 == $rc ) + { + # duplicate + push @h1_msgs_duplicate, $m; + # duplicate, same id same size? + my $h1_size = $h1_fir_ref->{$m}->{'RFC822.SIZE'} || 0; + + $sync->{ acc1 }->{ nb_msg_duplicate } += 1; + if ( ! $sync->{ syncduplicates } ) { + $sync->{ nb_msg_skipped } += 1 ; + $sync->{ h1_nb_msg_processed } +=1 ; + } + } + } + + + my $h1_msgs_duplicate_nb = scalar @h1_msgs_duplicate ; + + myprint( "Host1: folder [$h1_fold] selected $h1_msgs_nb messages, duplicates $h1_msgs_duplicate_nb\n" ) ; + + $sync->{ debug } and myprint( 'Host1: whole time parsing headers took ', timenext( $sync ), " s\n" ) ; + + + + # Getting headers and metada can be so long that host2 might be disconnected here + if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } + + + $sync->{ debug } and myprint( "Host2: parsing headers of folder [$h2_fold]\n" ) ; + + my ($h2_heads_ref, $h2_fir_ref) = ( {}, {} ); + $h2_heads_ref = $sync->{imap2}->parse_headers([@h2_msgs_not_in_cache], @useheader) if (@h2_msgs_not_in_cache); + $sync->{ debug } and myprint( "Host2: parsing headers of folder [$h2_fold] took ", timenext( $sync ), " s\n" ) ; + + $sync->{ debug } and myprint( "Host2: getting flags idate and sizes of folder [$h2_fold]\n" ) ; + @{ $h2_fir_ref }{@h2_msgs} = ( ); # fetch_hash can select by uid with last arg as ref + + + my @h2_common_fetch_param = ( 'FLAGS', 'INTERNALDATE', 'RFC822.SIZE' ) ; + if ( $sync->{ synclabels } or $sync->{ resynclabels } ) { push @h2_common_fetch_param, 'X-GM-LABELS' ; } + + if ( $sync->{abletosearch2} and scalar( @h2_msgs ) ) { + $h2_fir_ref = $sync->{imap2}->fetch_hash( \@h2_msgs, @h2_common_fetch_param, $h2_fir_ref) ; + }else{ + my $fetch_hash_uids = $fetch_hash_set || "1:*" ; + $h2_fir_ref = $sync->{imap2}->fetch_hash( $fetch_hash_uids, @h2_common_fetch_param, $h2_fir_ref ) + if ( @h2_msgs ) ; + } + + $sync->{ debug } and myprint( "Host2: getting flags idate and sizes of folder [$h2_fold] took ", timenext( $sync ), " s\n" ) ; + + my @h2_msgs_duplicate; + foreach my $m (@h2_msgs_not_in_cache) { + my $rc = parse_header_msg( $sync, $sync->{imap2}, $m, $h2_heads_ref, $h2_fir_ref, 'Host2', \%h2_hash ) ; + my $h2_size = $h2_fir_ref->{$m}->{'RFC822.SIZE'} || 0 ; + if (! defined $rc ) { + myprint( "Host2: $h2_fold/$m size $h2_size ignored (no wanted headers so we ignore this message)\n" ) ; + $h2_nb_msg_noheader += 1 ; + } elsif( 0 == $rc ) { + # duplicate + $sync->{ acc2 }->{ nb_msg_duplicate } += 1 ; + push @h2_msgs_duplicate, $m ; + } + } + + # %h2_folders_of_md5 + foreach my $md5 ( keys %h2_hash ) { + $sync->{ h2_folders_of_md5 }->{ $md5 }->{ $h2_fold } ++ ; + } + # %h1_folders_of_md5 + foreach my $md5 ( keys %h1_hash ) { + $sync->{ h1_folders_of_md5 }->{ $md5 }->{ $h2_fold } ++ ; + } + + + my $h2_msgs_duplicate_nb = scalar @h2_msgs_duplicate ; + + myprint( "Host2: folder [$h2_fold] selected $h2_msgs_nb messages, duplicates $h2_msgs_duplicate_nb\n" ) ; + + $sync->{ debug } and myprint( 'Host2 whole time parsing headers took ', timenext( $sync ), " s\n" ) ; + + $sync->{ debug } and myprint( "++++ Verifying [$h1_fold] -> [$h2_fold]\n" ) ; + # messages in host1 that are not in host2 + + my @h1_hash_keys_sorted_by_uid + = sort {$h1_hash{$a}{'m'} <=> $h1_hash{$b}{'m'}} keys %h1_hash; + + #myprint( map { $h1_hash{$_}{'m'} . q{ }} @h1_hash_keys_sorted_by_uid ) ; + + my @h2_hash_keys_sorted_by_uid + = sort {$h2_hash{$a}{'m'} <=> $h2_hash{$b}{'m'}} keys %h2_hash; + + # Deletions on account2. + + if( $sync->{ delete2duplicates } and not exists $h2_folders_from_1_several{ $h2_fold } ) { + my @h2_expunge ; + + foreach my $h2_msg ( @h2_msgs_duplicate ) { + myprint( "Host2: msg $h2_fold/$h2_msg marked \\Deleted [duplicate] on host2 $sync->{dry_message}\n" ) ; + push @h2_expunge, $h2_msg if $sync->{ uidexpunge2 } ; + if ( ! $sync->{ dry } ) { + $sync->{ imap2 }->delete_message( $h2_msg ) ; + $sync->{ acc2 }->{ nb_msg_deleted } += 1 ; + } + } + my $cnt = scalar @h2_expunge ; + if( @h2_expunge and not $sync->{ expunge2 } ) { + myprint( "Host2: UidExpunging $cnt message(s) in folder $h2_fold $sync->{dry_message}\n" ) ; + $sync->{imap2}->uidexpunge( \@h2_expunge ) if ! $sync->{dry} ; + } + if ( $sync->{ expunge2 } ){ + myprint( "Host2: Expunging folder $h2_fold $sync->{dry_message}\n" ) ; + $sync->{imap2}->expunge( ) if ! $sync->{dry} ; + } + } + + if( $sync->{ delete2 } and not exists $h2_folders_from_1_several{ $h2_fold } ) { + # No host1 folders f1a f1b ... going all to same f2 (via --regextrans2) + my @h2_expunge; + foreach my $m_id (@h2_hash_keys_sorted_by_uid) { + #myprint( "$m_id " ) ; + if ( ! exists $h1_hash{$m_id} ) { + my $h2_msg = $h2_hash{$m_id}{'m'}; + my $h2_flags = $h2_hash{$m_id}{'F'} || q{}; + my $isdel = $h2_flags =~ /\B\\Deleted\b/x ? 1 : 0; + myprint( "Host2: msg $h2_fold/$h2_msg marked \\Deleted on host2 [$m_id] $sync->{dry_message}\n" ) + if ! $isdel; + push @h2_expunge, $h2_msg if $sync->{ uidexpunge2 }; + if ( ! ( $sync->{ dry } or $isdel ) ) { + $sync->{ imap2 }->delete_message( $h2_msg ); + $sync->{ acc2 }->{ nb_msg_deleted } += 1; + } + } + } + foreach my $h2_msg ( @h2_msgs_delete2_not_in_cache ) { + myprint( "Host2: msg $h2_fold/$h2_msg marked \\Deleted [not in cache] on host2 $sync->{dry_message}\n" ) ; + push @h2_expunge, $h2_msg if $sync->{ uidexpunge2 }; + if ( ! $sync->{dry} ) { + $sync->{ imap2 }->delete_message( $h2_msg ); + $sync->{ acc2 }->{ nb_msg_deleted } += 1; + } + } + my $cnt = scalar @h2_expunge ; + + if( @h2_expunge and not $sync->{ expunge2 } ) { + myprint( "Host2: UidExpunging $cnt message(s) in folder $h2_fold $sync->{dry_message}\n" ) ; + $sync->{imap2}->uidexpunge( \@h2_expunge ) if ! $sync->{dry} ; + } + if ( $sync->{ expunge2 } ) { + myprint( "Host2: Expunging folder $h2_fold $sync->{dry_message}\n" ) ; + $sync->{imap2}->expunge( ) if ! $sync->{dry} ; + } + } + + if( $sync->{ delete2 } and exists $h2_folders_from_1_several{ $h2_fold } ) { + myprint( "Host2: folder $h2_fold $h2_folders_from_1_several{ $h2_fold } folders left to sync there\n" ) ; + my @h2_expunge; + foreach my $m_id ( @h2_hash_keys_sorted_by_uid ) { + my $h2_msg = $h2_hash{ $m_id }{ 'm' } ; + if ( ! exists $h1_hash{ $m_id } ) { + my $h2_flags = $h2_hash{ $m_id }{ 'F' } || q{} ; + my $isdel = $h2_flags =~ /\B\\Deleted\b/x ? 1 : 0 ; + if ( ! $isdel ) { + $sync->{ debug } and myprint( "Host2: msg $h2_fold/$h2_msg candidate for deletion [$m_id]\n" ) ; + $uid_candidate_for_deletion{ $h2_fold }{ $h2_msg }++ ; + } + }else{ + $sync->{ debug } and myprint( "Host2: msg $h2_fold/$h2_msg will cancel deletion [$m_id]\n" ) ; + $uid_candidate_no_deletion{ $h2_fold }{ $h2_msg }++ ; + } + } + foreach my $h2_msg ( @h2_msgs_delete2_not_in_cache ) { + myprint( "Host2: msg $h2_fold/$h2_msg candidate for deletion [not in cache]\n" ) ; + $uid_candidate_for_deletion{ $h2_fold }{ $h2_msg }++ ; + } + + foreach my $h2_msg ( @h2_msgs_in_cache ) { + myprint( "Host2: msg $h2_fold/$h2_msg will cancel deletion [in cache]\n" ) ; + $uid_candidate_no_deletion{ $h2_fold }{ $h2_msg }++ ; + } + + + if ( 0 == $h2_folders_from_1_several{ $h2_fold } ) { + # last host1 folder going to $h2_fold + myprint( "Last host1 folder going to $h2_fold\n" ) ; + foreach my $h2_msg ( keys %{ $uid_candidate_for_deletion{ $h2_fold } } ) { + $sync->{ debug } and myprint( "Host2: msg $h2_fold/$h2_msg candidate for deletion\n" ) ; + if ( exists $uid_candidate_no_deletion{ $h2_fold }{ $h2_msg } ) { + $sync->{ debug } and myprint( "Host2: msg $h2_fold/$h2_msg canceled deletion\n" ) ; + }else{ + myprint( "Host2: msg $h2_fold/$h2_msg marked \\Deleted $sync->{dry_message}\n" ) ; + push @h2_expunge, $h2_msg if $sync->{ uidexpunge2 } ; + if ( ! $sync->{ dry} ) { + $sync->{ imap2 }->delete_message( $h2_msg ) ; + $sync->{ acc2 }->{ nb_msg_deleted } += 1 ; + } + } + } + } + + my $cnt = scalar @h2_expunge ; + if( @h2_expunge and not $sync->{ expunge2 } ) { + myprint( "Host2: UidExpunging $cnt message(s) in folder $h2_fold $sync->{dry_message}\n" ) ; + $sync->{imap2}->uidexpunge( \@h2_expunge ) if ! $sync->{dry} ; + } + if ( $sync->{ expunge2 } ) { + myprint( "Host2: Expunging host2 folder $h2_fold $sync->{dry_message}\n" ) ; + $sync->{imap2}->expunge( ) if ! $sync->{dry} ; + } + + $h2_folders_from_1_several{ $h2_fold }-- ; + } + + my $h2_uidnext = $sync->{imap2}->uidnext( $h2_fold ) ; + $sync->{ debug } and myprint( "Host2: uidnext is $h2_uidnext\n" ) ; + $h2_uidguess = $h2_uidnext ; + + # Getting host2 headers, metada and delete2 stuff can be so long that host1 might be disconnected here + if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } + + my @h1_msgs_to_delete ; + MESS: foreach my $m_id (@h1_hash_keys_sorted_by_uid) { + abortifneeded( $sync ) ; + if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } + + #myprint( "h1_nb_msg_processed: $sync->{ h1_nb_msg_processed }\n" ) ; + my $h1_size = $h1_hash{$m_id}{'s'}; + my $h1_msg = $h1_hash{$m_id}{'m'}; + my $h1_idate = $h1_hash{$m_id}{'D'}; + + #my $labels = labels( $sync->{imap1}, $h1_msg ) ; + #print "LABELS: $labels\n" ; + + if ( ( not exists $h2_hash{ $m_id } ) + and ( not ( exists $sync->{ h2_folders_of_md5 }->{ $m_id } ) + or not $skipcrossduplicates ) ) + { + # copy + my $h2_msg = copy_message( $sync, $h1_msg, $h1_fold, $h2_fold, $h1_fir_ref, $permanentflags2, $cache_dir ) ; + if ( $h2_msg and $sync->{ delete1 } and not $sync->{ expungeaftereach } ) { + # not expunged + push @h1_msgs_to_delete, $h1_msg ; + } + + # A bug here with imapsync 1.920, fixed in 1.921 + # Added $h2_msg in the condition. Errors of APPEND were not counted as missing messages on host2! + if ( $h2_msg and not $sync->{ dry } ) + { + $sync->{ h2_folders_of_md5 }->{ $m_id }->{ $h2_fold } ++ ; + } + + # + if( $sync->{ delete2 } and ( exists $h2_folders_from_1_several{ $h2_fold } ) and $h2_msg ) { + myprint( "Host2: msg $h2_fold/$h2_msg will cancel deletion [fresh copy] on host2\n" ) ; + $uid_candidate_no_deletion{ $h2_fold }{ $h2_msg }++ ; + } + + if ( total_bytes_max_reached( $sync ) ) { + # Still a bug when using --delete1 --noexpungeaftereach + # same thing below on all total_bytes_max_reached! + last FOLDER ; + } + next MESS; + } + else + { + # already on host2 + if ( exists $h2_hash{ $m_id } ) + { + my $h2_msg = $h2_hash{$m_id}{'m'} ; + $sync->{ debug } and myprint( "Host1: found that msg $h1_fold/$h1_msg equals Host2 $h2_fold/$h2_msg\n" ) ; + if ( $usecache ) + { + $debugcache and myprint( "touch $cache_dir/${h1_msg}_$h2_msg\n" ) ; + touch( "$cache_dir/${h1_msg}_$h2_msg" ) + or croak( "Couldn't touch $cache_dir/${h1_msg}_$h2_msg" ) ; + } + } + elsif( exists $sync->{ h2_folders_of_md5 }->{ $m_id } ) + { + my @folders_dup = keys %{ $sync->{ h2_folders_of_md5 }->{ $m_id } } ; + ( $sync->{ debug } or $debugcrossduplicates ) and myprint( "Host1: found that msg $h1_fold/$h1_msg is also in Host2 folders @folders_dup\n" ) ; + $sync->{ h2_nb_msg_crossdup } +=1 ; + } + $sync->{ total_bytes_skipped } += $h1_size ; + $sync->{ nb_msg_skipped } += 1 ; + $sync->{ h1_nb_msg_processed } +=1 ; + } + + if ( exists $h2_hash{ $m_id } ) { + #$debug and myprint( "MESSAGE $m_id\n" ) ; + my $h2_msg = $h2_hash{$m_id}{'m'}; + if ( $sync->{resyncflags} ) { + sync_flags_fir( $sync, $h1_fold, $h1_msg, $h2_fold, $h2_msg, $permanentflags2, $h1_fir_ref, $h2_fir_ref ) ; + } + # Good + my $h2_size = $h2_hash{$m_id}{'s'}; + $sync->{ debug } and myprint( + "Host1: size msg $h1_fold/$h1_msg = $h1_size <> $h2_size = Host2 $h2_fold/$h2_msg\n" ) ; + + if ( $sync->{ resynclabels } ) + { + resynclabels( $sync, $h1_msg, $h2_msg, $h1_fir_ref, $h2_fir_ref, $h1_fold ) + } + } + + if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } + + if ( $sync->{ delete1 } ) { + push @h1_msgs_to_delete, $h1_msg ; + } + } + # END MESS: loop + + # @h1_msgs_in_cache are already synced too. + delete_message_on_host1( $sync, $h1_fold, $sync->{ expunge1 }, @h1_msgs_to_delete, @h1_msgs_in_cache ) ; + + if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } + + # MESS_IN_CACHE: + if ( ! $sync->{ delete1 } ) + { + foreach my $h1_msg ( @h1_msgs_in_cache ) + { + my $h2_msg = $cache_1_2_ref->{ $h1_msg } ; + $debugcache and myprint( "cache messages update flags $h1_msg->$h2_msg\n" ) ; + if ( $sync->{resyncflags} ) + { + sync_flags_fir( $sync, $h1_fold, $h1_msg, $h2_fold, $h2_msg, $permanentflags2, $h1_fir_ref, $h2_fir_ref ) ; + } + my $h1_size = $h1_fir_ref->{ $h1_msg }->{ 'RFC822.SIZE' } || 0 ; + $sync->{ total_bytes_skipped } += $h1_size; + $sync->{ nb_msg_skipped } += 1; + $sync->{ h1_nb_msg_processed } +=1 ; + } + } + + if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } + + @h1_msgs_to_delete = ( ) ; + #myprint( "Messages by uid: ", map { "$_ " } keys %h1_msgs_copy_by_uid, "\n" ) ; + # MESS_BY_UID: + foreach my $h1_msg ( sort { $a <=> $b } keys %h1_msgs_copy_by_uid ) + { + abortifneeded( $sync ) ; + $sync->{ debug } and myprint( "Copy by uid $h1_fold/$h1_msg\n" ) ; + if ( ! reconnect_12_if_needed( $sync ) ) { last FOLDER ; } + + my $h2_msg = copy_message( $sync, $h1_msg, $h1_fold, $h2_fold, $h1_fir_ref, $permanentflags2, $cache_dir ) ; + if( $sync->{ delete2 } and exists $h2_folders_from_1_several{ $h2_fold } and $h2_msg ) { + myprint( "Host2: msg $h2_fold/$h2_msg will cancel deletion [fresh copy] on host2\n" ) ; + $uid_candidate_no_deletion{ $h2_fold }{ $h2_msg }++ ; + } + last FOLDER if total_bytes_max_reached( $sync ) ; + } + + if ( $sync->{ expunge1 } ){ + myprint( "Host1: Expunging folder $h1_fold $sync->{dry_message}\n" ) ; + if ( ! $sync->{dry} ) { $sync->{imap1}->expunge( ) } ; + } + if ( $sync->{ expunge2 } ){ + myprint( "Host2: Expunging folder $h2_fold $sync->{dry_message}\n" ) ; + if ( ! $sync->{dry} ) { $sync->{imap2}->expunge( ) } ; + } + $sync->{ debug } and myprint( 'Time: ', timenext( $sync ), " s\n" ) ; +} + +eta_print( $sync ) ; + +myprint( "++++ End looping on each folder\n" ) ; + +if ( $sync->{ delete1 } and $sync->{ delete1emptyfolders } ) { + delete1emptyfolders( $sync ) ; +} + +( $sync->{ debug } or $sync->{debugfolders} ) and myprint( 'Time: ', timenext( $sync ), " s\n" ) ; + + +if ( $sync->{ foldersizesatend } ) { + myprint( << 'END_SIZE' ) ; + +Folders sizes after the synchronization. +You can remove this foldersizes listing by using "--nofoldersizesatend" +END_SIZE + + foldersizesatend( $sync ) ; +} + +#$sync->{imap1}->State( 0 ); # Unconnected +if ( ! lost_connection( $sync, $sync->{imap1}, "for host1 [$sync->{host1}]" ) ) { $sync->{imap1}->logout( ) ; } +if ( ! lost_connection( $sync, $sync->{imap2}, "for host2 [$sync->{host2}]" ) ) { $sync->{imap2}->logout( ) ; } + +do_and_print_stats( $sync ) ; + + +if ( $sync->{ nb_errors } ) +{ + myprint( errors_listing( $sync ) ) ; +} + + +if ( $sync->{ testslive } or $sync->{ testslive6 } ) +{ + tests_live_result( $sync->{ nb_errors } ) ; +} + + +if ( $sync->{ nb_errors } ) +{ + my $exit_value = exit_value( $sync, $sync->{ most_common_error } ) ; + exit_clean( $sync, $exit_value ) ; +} +else +{ + exit_clean( $sync, $EX_OK ) ; +} + +return ; +} + +# END of sub single_sync + + +# subroutines +sub myprint +{ + #print @ARG ; + print { $sync->{ tee } || \*STDOUT } @ARG ; + return ; +} + +sub myprintf +{ + printf { $sync->{ tee } || \*STDOUT } @ARG ; + return ; +} + +sub mysprintf +{ + my( $format, @list ) = @ARG ; + return sprintf $format, @list ; +} + +sub output_start +{ + my $mysync = shift @ARG ; + + if ( not $mysync ) { return ; } + + my @output = @ARG ; + $mysync->{ output } = join( q{}, @output ) . ( $mysync->{ output } || q{} ) ; + return $mysync->{ output } ; +} + + +sub tests_output_start +{ + note( 'Entering tests_output_start()' ) ; + + my $mysync = { } ; + + is( undef, output_start( ), 'output_start: no args => undef' ) ; + is( q{}, output_start( $mysync ), 'output_start: one arg => ""' ) ; + is( 'rrrr', output_start( $mysync, 'rrrr' ), 'output_start: rrrr => rrrr' ) ; + is( 'aaaarrrr', output_start( $mysync, 'aaaa' ), 'output_start: aaaa => aaaarrrr' ) ; + is( "\naaaarrrr", output_start( $mysync, "\n" ), 'output_start: \n => \naaaarrrr' ) ; + is( "ABC\naaaarrrr", output_start( $mysync, 'A', 'B', 'C' ), 'output_start: A B C => ABC\naaaarrrr' ) ; + + note( 'Leaving tests_output_start()' ) ; + return ; +} + +sub tests_output +{ + note( 'Entering tests_output()' ) ; + + my $mysync = { } ; + + is( undef, output( ), 'output: no args => undef' ) ; + is( q{}, output( $mysync ), 'output: one arg => ""' ) ; + is( 'rrrr', output( $mysync, 'rrrr' ), 'output: rrrr => rrrr' ) ; + is( 'rrrraaaa', output( $mysync, 'aaaa' ), 'output: aaaa => rrrraaaa' ) ; + is( "rrrraaaa\n", output( $mysync, "\n" ), 'output: \n => rrrraaaa\n' ) ; + is( "rrrraaaa\nABC", output( $mysync, 'A', 'B', 'C' ), 'output: A B C => rrrraaaaABC\n' ) ; + + note( 'Leaving tests_output()' ) ; + return ; +} + +sub output +{ + my $mysync = shift @ARG ; + + if ( not $mysync ) { return ; } + + my @output = @ARG ; + $mysync->{ output } .= join( q{}, @output ) ; + return $mysync->{ output } ; +} + + + +sub tests_output_reset_with +{ + note( 'Entering tests_output_reset_with()' ) ; + + my $mysync = { } ; + + is( undef, output_reset_with( ), 'output_reset_with: no args => undef' ) ; + is( q{}, output_reset_with( $mysync ), 'output_reset_with: one arg => ""' ) ; + is( 'rrrr', output_reset_with( $mysync, 'rrrr' ), 'output_reset_with: rrrr => rrrr' ) ; + is( 'aaaa', output_reset_with( $mysync, 'aaaa' ), 'output_reset_with: aaaa => aaaa' ) ; + is( "\n", output_reset_with( $mysync, "\n" ), 'output_reset_with: \n => \n' ) ; + + note( 'Leaving tests_output_reset_with()' ) ; + return ; +} + +sub output_reset_with +{ + my $mysync = shift @ARG ; + + if ( not $mysync ) { return ; } + + my @output = @ARG ; + $mysync->{ output } = join( q{}, @output ) ; + return $mysync->{ output } ; +} + + +sub tests_print_output_if_needed +{ + note( 'Entering tests_print_output_if_needed()' ) ; + + is( undef, print_output_if_needed( ), 'print_output_if_needed: no args => undef' ) ; + my $mysync = { } ; + is( q{}, print_output_if_needed( $mysync ), 'print_output_if_needed: undef => undef' ) ; + + output( $mysync, "Hello\n" ) ; + is( "Hello\n", print_output_if_needed( $mysync ), 'print_output_if_needed: Hello => Hello' ) ; + + $mysync->{ dockercontext } = 1 ; + is( "Hello\n", print_output_if_needed( $mysync ), 'print_output_if_needed: dockercontext + Hello => Hello' ) ; + + $mysync->{ version } = 1 ; + is( q{}, print_output_if_needed( $mysync ), 'print_output_if_needed: dockercontext + Hello + --version => ""' ) ; + + $mysync->{ dockercontext } = 0 ; + is( "Hello\n", print_output_if_needed( $mysync ), 'print_output_if_needed: Hello + --version => Hello' ) ; + + note( 'Leaving tests_print_output_if_needed()' ) ; + return ; +} + + +sub print_output_if_needed +{ + + my $mysync = shift @ARG ; + if ( ! defined $mysync ) { return ; } + my $output = output( $mysync ) ; + + if ( $mysync->{ version } && under_docker_context( $mysync ) ) + { + return q{} ; + } + else + { + myprint( $output ) ; + return $output ; + } + +} + + + +sub define_pidfile +{ + my $mysync = shift @ARG ; + + $mysync->{ pidfilelocking } = defined $mysync->{ pidfilelocking } ? $mysync->{ pidfilelocking } : 0 ; + + my $host1 = $mysync->{ host1 } || q{} ; + my $user1 = $mysync->{ user1 } || q{} ; + my $host2 = $mysync->{ host2 } || q{} ; + my $user2 = $mysync->{ user2 } || q{} ; + + my $account1_filtered = filter_forbidden_characters( slash_to_underscore( $host1 . '_' . $user1 ) ) || q{} ; + my $account2_filtered = filter_forbidden_characters( slash_to_underscore( $host2 . '_' . $user2 ) ) || q{} ; + + my $pidfile_basename ; + + if ( $ENV{ 'NET_SERVER_SOFTWARE' } and ( $ENV{ 'NET_SERVER_SOFTWARE' } =~ /Net::Server::HTTP/ ) ) + { + # under local webserver + $pidfile_basename = 'imapsync' . '_' . $account1_filtered . '_' . $account2_filtered . '.pid' ; + } + else + { + $pidfile_basename = 'imapsync.pid' ; + } + + $mysync->{ pidfile } = defined $mysync->{ pidfile } ? $mysync-> { pidfile } : $mysync->{ tmpdir } . "/$pidfile_basename" ; + $mysync->{ abortfile } = abortfile( $mysync, $PROCESS_ID ) ; + return ; +} + +sub abortfile +{ + my $mysync = shift @ARG ; + my $pid = shift @ARG ; + + my $abortfile ; + if ( $mysync->{ abort } ) + { + $abortfile = $mysync->{ pidfile } . "abort$pid" ; + } + else + { + $abortfile = $mysync->{ pidfile } . "abort$PROCESS_ID" ; + } + return $abortfile ; +} + +sub tests_kill_zero +{ + note( 'Entering tests_kill_zero()' ) ; + + + + SKIP: { + if ( 'MSWin32' eq $OSNAME ) { skip( 'Tests tests_kill_zero avoided on Windows', 8 ) ; } + + + is( 1, kill( 'ZERO', $PROCESS_ID ), "kill ZERO : myself $PROCESS_ID => 1" ) ; + is( 2, kill( 'ZERO', $PROCESS_ID, $PROCESS_ID ), "kill ZERO : myself $PROCESS_ID $PROCESS_ID => 2" ) ; + + if ( (-e '/.dockerenv' ) or ( 0 == $EFFECTIVE_USER_ID) ) + { + is( 1, kill( 'ZERO', 1 ), "kill ZERO : pid 1 => 1 (docker context or root)" ) ; + is( 2, kill( 'ZERO', $PROCESS_ID, 1 ), "kill ZERO : myself + pid 1, $PROCESS_ID 1 => 2 (docker context or root)" ) ; + } + else + { + is( 0, kill( 'ZERO', 1 ), "kill ZERO : pid 1 => 0 (non root)" ) ; + is( 1, kill( 'ZERO', $PROCESS_ID, 1 ), "kill ZERO : myself + pid 1, $PROCESS_ID 1 => 1 (one is non root)" ) ; + + } + + + my $pid_1 = fork( ) ; + if ( $pid_1 ) + { + # parent + } + else + { + # child + sleep 3 ; + exit ; + } + + my $pid_2 ; + $pid_2 = fork( ) ; + if ( $pid_2 ) + { + # I am the parent + ok( defined( $pid_2 ), "kill_zero: initial fork ok. I am the parent $PROCESS_ID" ) ; + ok( $pid_2 , "kill_zero: initial fork ok, child pid is $pid_2" ) ; + is( 3, kill( 'ZERO', $PROCESS_ID, $pid_2, $pid_1 ), "kill ZERO : myself $PROCESS_ID and child $pid_2 and brother $pid_1 => 3" ) ; + + is( $pid_2, waitpid( $pid_2, 0 ), "kill_zero: child $pid_2 no more there => waitpid return $pid_2" ) ; + } + else + { + # I am the child + note( 'This one fails under Windows, kill ZERO returns 0 instead of 2' ) ; + is( 2, kill( 'ZERO', $PROCESS_ID, $pid_1 ), "kill ZERO : myself child $PROCESS_ID brother $pid_1 => 2" ) ; + myprint( "I am the child pid $PROCESS_ID, Exiting\n" ) ; + exit ; + } + wait( ) ; + + # End of SKIP block + } + + note( 'Leaving tests_kill_zero()' ) ; + return ; +} + + + + +sub tests_killpid_by_parent +{ + note( 'Entering tests_killpid_by_parent()' ) ; + + SKIP: { + if ( 'MSWin32' eq $OSNAME ) { skip( 'Tests tests_killpid_by_parent avoided on Windows', 7 ) ; } + + is( undef, killpid( ), 'killpid: no args => undef' ) ; + note( "killpid: trying to kill myself pid $PROCESS_ID, hope I will not succeed" ) ; + is( undef, killpid( $PROCESS_ID ), 'killpid: myself => undef' ) ; + + local $SIG{'QUIT'} = sub { myprint "GOT SIG QUIT! I am PID $PROCESS_ID. Exiting\n" ; exit ; } ; + + my $pid ; + $pid = fork( ) ; + if ( $pid ) + { + # I am the parent + ok( defined( $pid ), "killpid: initial fork ok. I am the parent $PROCESS_ID" ) ; + ok( $pid , "killpid: initial fork ok, child pid is $pid" ) ; + + is( 2, kill( 'ZERO', $PROCESS_ID, $pid ), "kill ZERO : myself $PROCESS_ID and child $pid => 2" ) ; + is( 1, killpid( $pid ), "killpid: child $pid killed => 1" ) ; + is( -1, waitpid( $pid, 0 ), "killpid: child $pid no more there => waitpid return -1" ) ; + } + else + { + # I am the child + myprint( "I am the child pid $PROCESS_ID, sleeping 1 + 3 seconds then kill myself\n" ) ; + sleep 1 ; + myprint( "I am the child pid $PROCESS_ID, slept 1 second, should be killed by my parent now, PPID " . mygetppid( ) . "\n" ) ; + sleep 3 ; + # this test should not be run. If it happens => failure. + ok( 0 == 1, "killpid: child pid $PROCESS_ID not dead => failure" ) ; + myprint( "I am the child pid $PROCESS_ID, killing myself failure... Exiting\n" ) ; + exit ; + } + + # End of SKIP block + } + note( 'Leaving tests_killpid_by_parent()' ) ; + return ; +} + +sub tests_killpid_by_brother +{ + note( 'Entering tests_killpid_by_brother()' ) ; + + + SKIP: { + if ( 'MSWin32' eq $OSNAME ) { skip( 'Tests tests_killpid_by_brother avoided on Windows', 2 ) ; } + + local $SIG{'QUIT'} = sub { myprint "GOT SIG QUIT! I am PID $PROCESS_ID. Exiting\n" ; exit ; } ; + + my $pid_parent = $PROCESS_ID ; + myprint( "I am the parent pid $pid_parent\n" ) ; + my $pid_1 = fork( ) ; + if ( $pid_1 ) + { + # parent + } + else + { + # child + #while ( 1 ) { } ; + sleep 2 ; + sleep 2 ; + # this test should not be run. If it happens => failure. + # Well under Windows this always fails, shit! + ok( 0 == 1 or ( 'MSWin32' eq $OSNAME ) , "killpid: child pid $PROCESS_ID killing by brother but not dead => failure" ) ; + myprint( "I am the child pid $PROCESS_ID, killing by brother failed... Exiting\n" ) ; + exit ; + } + + my $pid_2 ; + $pid_2 = fork( ) ; + if ( $pid_2 ) + { + # parent + } + else + { + # I am the child + myprint( "I am the child pid $PROCESS_ID, my brother has pid $pid_1\n" ) ; + is( 1, killpid( $pid_1 ), "killpid: brother $pid_1 killed => 1" ) ; + sleep 2 ; + exit ; + } + + #sleep 1 ; + is( $pid_1, waitpid( $pid_1, 0), "I am the parent $PROCESS_ID waitpid _1( $pid_1 )" ) ; + is( $pid_2, waitpid( $pid_2, 0 ), "I am the parent $PROCESS_ID waitpid _2( $pid_2 )" ) ; + + + # End of SKIP block + } + + note( 'Leaving tests_killpid_by_brother()' ) ; + return ; +} + + +sub killpid +{ + my $pidtokill = shift ; + + if ( ! $pidtokill ) { + myprint( "No process to kill.\n" ) ; + return ; + } + + if ( $PROCESS_ID == $pidtokill ) { + myprint( "I will not kill myself pid $PROCESS_ID via killpid. Sractch it!\n" ) ; + return ; + } + + + # First ask for suicide + if ( kill( 'ZERO', $pidtokill ) or ( 'MSWin32' eq $OSNAME ) ) { + myprint( "Sending signal QUIT to PID $pidtokill \n" ) ; + kill 'QUIT', $pidtokill ; + sleep 3 ; + waitpid( $pidtokill, WNOHANG) ; + }else{ + myprint( "Can not send signal kill ZERO to PID $pidtokill.\n" ) ; + return ; + } + + #while ( waitpid( $pidtokill, WNOHANG) > 0 ) { } ; + + # Then murder + if ( kill( 'ZERO', $pidtokill ) or ( 'MSWin32' eq $OSNAME ) ) { + myprint( "Sending signal KILL to PID $pidtokill \n" ) ; + kill 'KILL', $pidtokill ; + sleep 1 ; + waitpid( $pidtokill, WNOHANG) ; + }else{ + myprint( "Process PID $pidtokill ended.\n" ) ; + return 1; + } + # Well ... + if ( kill( 'ZERO', $pidtokill ) or ( 'xMSWin32' eq $OSNAME ) ) { + myprint( "Process PID $pidtokill seems still there. Can not do much.\n" ) ; + return ; + }else{ + myprint( "Process PID $pidtokill ended.\n" ) ; + return 1; + } + + return ; +} + +sub tests_abort +{ + note( 'Entering tests_abort()' ) ; + # Well, the abort behavior is tested by test.sh + is( undef, abort( ), 'abort: no args => undef' ) ; + note( 'Leaving tests_abort()' ) ; + return ; +} + + + + +sub abort +{ + my $mysync = shift @ARG ; + + myprint( "In abort\n" ) ; + if ( not $mysync ) { return ; } + + if ( ! -r $mysync->{pidfile} ) { + myprint( "In abort: Can not read pidfile $mysync->{pidfile}\n" ) ; + return ; + } + my $pidtokill = firstline( $mysync->{pidfile} ) ; + if ( ! $pidtokill ) { + myprint( "In abort: No process to abort in $mysync->{pidfile}\n" ) ; + return ; + } + + if ( ! match_a_pid_number( $pidtokill ) ) + { + myprint( "In abort: pid $pidtokill in $mysync->{pidfile} is not a pid number\n" ) ; + return ; + } + + + if ( $mysync->{abortbyfile} ) + { + abortbyfile( $mysync, $pidtokill ) ; + } + else + { + killpid( $pidtokill ) ; + } + return ; +} + +sub abortbyfile +{ + my $mysync = shift @ARG ; + my $pidtokill = shift @ARG ; + + my $abortfile = abortfile( $mysync, $pidtokill ) ; + myprint( "touching $abortfile\n" ) ; + touch( $abortfile ) ; + return ; +} + + +sub tests_under_docker_context +{ + note( 'Entering tests_under_docker_context()' ) ; + + is( undef, under_docker_context( ), 'under_docker_context: no args => undef' ) ; + + my $mysync = { } ; + $mysync->{ dockercontext } = 1 ; + is( 1, under_docker_context( $mysync ), 'under_docker_context: --dockercontext => 1' ) ; + $mysync->{ dockercontext } = 0 ; + is( 0, under_docker_context( $mysync ), 'under_docker_context: --nodockercontext => 0' ) ; + + $mysync = { } ; + # Is not it a stupid test? + if ( under_docker_context( $mysync ) ) + { + is( 1, under_docker_context( $mysync ), 'under_docker_context: docker context => 1' ) ; + } + else + { + is( 0, under_docker_context( $mysync ), 'under_docker_context: not docker context => 0' ) ; + } + + note( 'Leaving tests_under_docker_context()' ) ; + return ; +} + + +sub under_docker_context +{ + my $mysync = shift ; + + if ( ! defined $mysync ) { return ; } + + if ( defined $mysync->{ dockercontext } ) + { + return( $mysync->{ dockercontext } ) ; + } + + if ( -e '/.dockerenv' ) + { + return 1 ; + } + else + { + return 0 ; + } + + return ; +} + + +sub docker_context +{ + my $mysync = shift ; + + if ( ! under_docker_context( $mysync ) ) + { + return ; + } + + output( $mysync, "Docker context detected with the file /.dockerenv\n" ) ; + # No pidfile by default + + $mysync->{ pidfile } = defined( $mysync->{ pidfile } ) ? $mysync->{ pidfile } : q{} ; + # No log by default + if ( defined( $mysync->{ log } ) ) + { + output( $mysync, "Logging in Docker context. Be sure you added access to it with a mount or similar. See https://docs.docker.com/storage/volumes/\n" ) ; + } + else + { + output( $mysync, "No log by default in Docker context. Use --log to trigger logging to the logfile.\n" ) ; + $mysync->{ log } = 0 ; + } + + # In case something is written relatively to . + my $tmp_dir = "/var/tmp/uid_$EFFECTIVE_USER_ID" ; + mkpath( $tmp_dir ) ; # silly? No. it is for imapsync --version being ok. + do_valid_directory( $tmp_dir ) ; + output( $mysync, "Changing current directory to $tmp_dir\n" ) ; + chdir $tmp_dir ; + + return ; +} + +sub cgibegin +{ + my $mysync = shift ; + if ( ! under_cgi_context( $mysync ) ) { return ; } + require CGI ; + CGI->import( qw( -no_debug -utf8 ) ) ; + require CGI::Carp ; + CGI::Carp->import( qw( fatalsToBrowser ) ) ; + $mysync->{cgi} = CGI->new( ) ; + return ; +} + +sub tests_under_cgi_context +{ + note( 'Entering tests_under_cgi_context()' ) ; + + # $ENV{SERVER_SOFTWARE} = 'under imapsync' ; + do { + # Not in cgi context + delete local $ENV{SERVER_SOFTWARE} ; + is( undef, under_cgi_context( ), 'under_cgi_context: SERVER_SOFTWARE unset => not in cgi context' ) ; + } ; + do { + # In cgi context + local $ENV{SERVER_SOFTWARE} = 'under imapsync' ; + is( 1, under_cgi_context( ), 'under_cgi_context: SERVER_SOFTWARE set => in cgi context' ) ; + } ; + do { + # Not in cgi context + delete local $ENV{SERVER_SOFTWARE} ; + is( undef, under_cgi_context( ), 'under_cgi_context: SERVER_SOFTWARE unset => not in cgi context' ) ; + } ; + do { + # In cgi context + local $ENV{SERVER_SOFTWARE} = 'under imapsync' ; + is( 1, under_cgi_context( ), 'under_cgi_context: SERVER_SOFTWARE set => in cgi context' ) ; + } ; + note( 'Leaving tests_under_cgi_context()' ) ; + return ; +} + + +sub under_cgi_context +{ + my $mysync = shift ; + # Under cgi context + if ( $ENV{SERVER_SOFTWARE} ) { + return 1 ; + } + # Not in cgi context + return ; +} + +sub cgibuildheader +{ + my $mysync = shift ; + if ( ! under_cgi_context( $mysync ) ) { return ; } + + my $imapsync_runs = $mysync->{cgi}->cookie( 'imapsync_runs' ) || 0 ; + my $cookie = $mysync->{cgi}->cookie( + -name => 'imapsync_runs', + -value => 1 + $imapsync_runs, + -expires => '+20y', + -path => '/cgi-bin/imapsync', + ) ; + my $httpheader ; + if ( $mysync->{ abort } ) { + $httpheader = $mysync->{cgi}->header( + -type => 'text/plain; charset=UTF-8', + -status => '200 OK to abort syncing IMAP boxes' . ". Here is " . hostname(), + ) ; + }elsif( $mysync->{ loaddelay } ) { +# https://tools.ietf.org/html/rfc2616#section-10.5.4 +# 503 Service Unavailable +# The server is currently unable to handle the request due to a temporary overloading or maintenance of the server. + $httpheader = $mysync->{cgi}->header( + -type => 'text/plain; charset=UTF-8', + -status => '503 Service Unavailable' . ". Be back in $mysync->{ loaddelay } min. Load on " . hostname() . " is $mysync->{ loadavg }", + ) ; + }else{ + $httpheader = $mysync->{cgi}->header( + -type => 'text/plain; charset=UTF-8', + -status => '200 OK to sync IMAP boxes' . ". Load on " . hostname() . " is $mysync->{ loadavg }", + -cookie => $cookie, + ) ; + } + output_start( $mysync, $httpheader ) ; + + return ; +} + +sub cgiload +{ + # Exit on heavy load in CGI context + my $mysync = shift ; + if ( ! under_cgi_context( $mysync ) ) { return ; } + if ( $mysync->{ abort } ) { return ; } # keep going to abort since some ressources will be free soon + if ( $mysync->{ loaddelay } ) + { + $mysync->{nb_errors}++ ; + exit_clean( $mysync, $EX_UNAVAILABLE, + "Server is on heavy load. Be back in $mysync->{ loaddelay } min. Load is $mysync->{ loadavg }\n" + ) ; + } + return ; +} + +sub tests_set_umask +{ + note( 'Entering tests_set_umask()' ) ; + + my $save_umask = umask ; + + my $mysync = {} ; + if ( 'MSWin32' eq $OSNAME ) { + is( undef, set_umask( $mysync ), "set_umask: set failure to $UMASK_PARANO on MSWin32" ) ; + }else{ + is( 1, set_umask( $mysync ), "set_umask: set to $UMASK_PARANO" ) ; + } + + umask $save_umask ; + note( 'Leaving tests_set_umask()' ) ; + return ; +} + +sub set_umask +{ + my $mysync = shift ; + my $previous_umask = umask_str( ) ; + my $new_umask = umask_str( $UMASK_PARANO ) ; + output( $mysync, "Umask set with $new_umask (was $previous_umask)\n" ) ; + if ( $new_umask eq $UMASK_PARANO ) { + return 1 ; + } + return ; +} + +sub tests_umask_str +{ + note( 'Entering tests_umask_str()' ) ; + + my $save_umask = umask ; + + is( umask_str( ), umask_str( ), 'umask_str: no parameters => idopotent' ) ; + is( my $save_umask_str = umask_str( ), umask_str( ), 'umask_str: no parameters => idopotent + save' ) ; + is( '0000', umask_str( q{ } ), 'umask_str: q{ } => 0000' ) ; + is( '0000', umask_str( q{} ), 'umask_str: q{} => 0000' ) ; + is( '0000', umask_str( '0000' ), 'umask_str: 0000 => 0000' ) ; + is( '0000', umask_str( '0' ), 'umask_str: 0 => 0000' ) ; + is( '0200', umask_str( '0200' ), 'umask_str: 0200 => 0200' ) ; + is( '0400', umask_str( '0400' ), 'umask_str: 0400 => 0400' ) ; + is( '0600', umask_str( '0600' ), 'umask_str: 0600 => 0600' ) ; + + SKIP: { + if ( 'MSWin32' eq $OSNAME ) { skip( 'Tests success only for Unix', 6 ) ; } + is( '0100', umask_str( '0100' ), 'umask_str: 0100 => 0100' ) ; + is( '0001', umask_str( '0001' ), 'umask_str: 0001 => 0001' ) ; + is( '0777', umask_str( '0777' ), 'umask_str: 0777 => 0777' ) ; + is( '0777', umask_str( '00777' ), 'umask_str: 00777 => 0777' ) ; + is( '0777', umask_str( ' 777 ' ), 'umask_str: 777 => 0777' ) ; + is( "$UMASK_PARANO", umask_str( $UMASK_PARANO ), "umask_str: UMASK_PARANO $UMASK_PARANO => $UMASK_PARANO" ) ; + } + + is( $save_umask_str, umask_str( $save_umask_str ), 'umask_str: restore with str' ) ; + is( $save_umask, umask, 'umask_str: umask is restored, controlled by direct umask' ) ; + is( $save_umask, umask $save_umask, 'umask_str: umask is restored by direct umask' ) ; + is( $save_umask, umask, 'umask_str: umask initial controlled by direct umask' ) ; + + note( 'Leaving tests_umask_str()' ) ; + return ; +} + +sub umask_str +{ + my $value = shift ; + + if ( defined $value ) { + umask oct( $value ) ; + } + my $current = umask ; + + return( sprintf( '%#04o', $current ) ) ; +} + +sub tests_umask +{ + note( 'Entering tests_umask()' ) ; + + my $save_umask ; + is( umask, umask, 'umask: umask is umask' ) ; + is( $save_umask = umask, umask, "umask: umask is umask again + save it: $save_umask" ) ; + is( $save_umask, umask oct(0000), 'umask: umask 0000' ) ; + is( oct(0000), umask, 'umask: umask is now 0000' ) ; + is( oct(0000), umask oct(777), 'umask: umask 0777 call, previous 0000' ) ; + + SKIP: { + if ( 'MSWin32' eq $OSNAME ) { skip( 'Tests success only for Unix', 2 ) ; } + is( oct(777), umask, 'umask: umask is now 0777' ) ; + is( oct(777), umask $save_umask, "umask: umask $save_umask restore inital value, previous 0777" ) ; + } + + ok( defined umask $save_umask, "umask: umask $save_umask restore inital value, previous defined" ) ; + is( $save_umask, umask, 'umask: umask is umask restored' ) ; + note( 'Leaving tests_umask()' ) ; + + return ; +} + +sub buggyflagsregex +{ + # From /X analyse + # cut -d: -f1 Error_112_all_syncs.txt | xargs egrep -oih 'Invalid system flag [^( ]+' | sort | uniq -c | sort -g + my @buggyflagsregex = ( 's/\\\\RECEIPTCHECKED|\\\\Indexed|\\\\X-EON-HAS-ATTACHMENT|\\\\UNSEEN|\\\\ATTACHED|\\\\X-HAS-ATTACH|\\\\FORWARDED|\\\\FORWARD|\\\\X-FORWARDED|\\\\\$FORWARDED|\\\\PRIORITY|\\\\READRCPT//g' ) ; + return( @buggyflagsregex ) ; +} + +sub cgisetcontext +{ + my $mysync = shift ; + if ( ! under_cgi_context( $mysync ) ) { return ; } + + output( $mysync, "Under cgi context\n" ) ; + + + set_umask( $mysync ) ; + + # Remove all content in unsafe evaled options + @{ $mysync->{ regextrans2 } } = ( ) ; + + @{ $mysync->{ regexflag } } = buggyflagsregex( ) ; + + @regexmess = ( ) ; + @skipmess = ( ) ; + @pipemess = ( ) ; + $delete2foldersonly = undef ; + $delete2foldersbutnot = undef ; + $maxlinelengthcmd = undef ; + + # Set safe default values (I hope...) + + + #$mysync->{pidfile} = 'imapsync.pid' ; + $mysync->{ pidfilelocking } = 1 ; + $mysync->{ errorsmax } = $ERRORS_MAX_CGI ; + $modulesversion = 0 ; + $mysync->{ releasecheck } = defined $mysync->{ releasecheck } ? $mysync->{ releasecheck } : 1 ; + $usecache = 0 ; + $mysync->{ showpasswords } = 0 ; + $mysync->{ acc1 }->{ debugimap } = 0 ; + $mysync->{ acc2 }->{ debugimap } = 0 ; + + $mysync->{ acc1 }->{ reconnectretry } = $DEFAULT_NB_RECONNECT_PER_IMAP_COMMAND ; + $mysync->{ acc2 }->{ reconnectretry } = $DEFAULT_NB_RECONNECT_PER_IMAP_COMMAND ; + + $pipemesscheck = 0 ; + + $mysync->{ hashfile } = $CGI_HASHFILE ; + my $hashsynclocal = hashsynclocal( $mysync ) || die "Can not get hashsynclocal. Exiting\n" ; + + if ( $ENV{ 'NET_SERVER_SOFTWARE' } and ( $ENV{ 'NET_SERVER_SOFTWARE' } =~ /Net::Server::HTTP/ ) ) + { + # under local webserver + $cgidir = q{.} ; + } + else + { + $cgidir = $CGI_TMPDIR_TOP . '/' . $hashsynclocal ; + } + -d $cgidir or mkpath $cgidir or die "Can not create $cgidir: $OS_ERROR\n" ; + $mysync->{ tmpdir } = $cgidir ; + $mysync->{ logdir } = '' ; + + chdir $cgidir or die "Can not cd to $cgidir: $OS_ERROR\n" ; + cgioutputenvcontext( $mysync ) ; + $mysync->{ debug } and output( $mysync, 'Current directory is ' . getcwd( ) . "\n" ) ; + $mysync->{ debug } and output( $mysync, 'Real user id is ' . getpwuid_any_os( $REAL_USER_ID ) . " (uid $REAL_USER_ID)\n" ) ; + $mysync->{ debug } and output( $mysync, 'Effective user id is ' . getpwuid_any_os( $EFFECTIVE_USER_ID ). " (euid $EFFECTIVE_USER_ID)\n" ) ; + + $mysync->{ skipemptyfolders } = defined $mysync->{ skipemptyfolders } ? $mysync->{ skipemptyfolders } : 1 ; + + # Out of memory with messages over 1 GB ? + $mysync->{ maxsize } = defined $mysync->{ maxsize } ? $mysync->{ maxsize } : 1_000_000_000 ; + + # tail -f behaviour on by default + $mysync->{ tail } = defined $mysync->{ tail } ? $mysync->{ tail } : 1 ; + + # not sure it's for good + @useheader = qw( Message-Id Received ) ; + + # addheader on by default + $mysync->{ addheader } = defined $mysync->{ addheader } ? $mysync->{ addheader } : 1 ; + + # sync duplicates by default in cgi context + $mysync->{ syncduplicates } = defined $mysync->{ syncduplicates } ? $mysync->{ syncduplicates } : 1 ; + + # log the logfile name by default in cgi context + $mysync->{ loglogfile } = defined $mysync->{ loglogfile } ? $mysync->{ loglogfile } : 1 ; + return ; +} + +sub cgioutputenvcontext +{ + my $mysync = shift @ARG ; + + for my $envvar ( qw( REMOTE_ADDR REMOTE_HOST HTTP_REFERER HTTP_USER_AGENT SERVER_SOFTWARE SERVER_PORT HTTP_COOKIE ) ) { + + my $envval = $ENV{ $envvar } || q{} ; + if ( $envval ) { output( $mysync, "$envvar is $envval\n" ) } ; + } + + return ; +} + +sub announcelogfile +{ + my $mysync = shift ; + + if ( $mysync->{ log } ) + { + myprint( "Log file is $mysync->{ logfile } ( to change it, use --logfile path ; or use --nolog to turn off logging )\n" ) ; + loglogfile( $mysync ) ; + } + else + { + myprint( "No log file because of option --nolog\n" ) ; + } + return ; +} + + +sub loglogfile +{ + my $mysync = shift ; + if ( ! $mysync->{ loglogfile } ) { return ; } + if ( ! $mysync->{ log } ) { return ; } + + my $cwd = getcwd( ) ; + my $absolutelogfilepath ; + # Fixme: add case when the logfile name is already absolute + $absolutelogfilepath = "$cwd/$mysync->{ logfile }" ; + my $loglogfilename = '../list_all_logs_auto.txt' ; + myprint( "Writing log file name $absolutelogfilepath to $loglogfilename\n" ) ; + if ( open( my $fh, '>>', $loglogfilename ) ) + { + print $fh "$absolutelogfilepath\n" ; + close $fh ; + } + else + { + myprint( "Could not open loglogfile $loglogfilename $!\n" ) ; + } + return ; +} + + +sub checkselectable +{ + my $mysync = shift ; + + if ( $mysync->{ checkselectable } ) { + my @h1_folders_wanted_selectable ; + myprint( "Host1: Checking wanted folders are selectable. Use --nocheckselectable to avoid this check.\n" ) ; + foreach my $folder ( @{ $mysync->{ h1_folders_wanted } } ) + { + ( $mysync->{ debug } or $mysync->{ debugfolders } ) and myprint( "Checking $folder is selectable on host1\n" ) ; + # It does an imap command LIST "" $folder and then search for no \Noselect + if ( ! $mysync->{ imap1 }->selectable( $folder ) ) + { + myprint( "Host1: warning! ignoring folder $folder because it is not selectable\n" ) ; + }else + { + push @h1_folders_wanted_selectable, $folder ; + } + } + @{ $mysync->{ h1_folders_wanted } } = @h1_folders_wanted_selectable ; + ( $mysync->{ debug } or $mysync->{ debugfolders } ) + and myprint( 'Host1: checking folders took ', timenext( $mysync ), " s\n" ) ; + } + else + { + myprint( "Host1: Not checking that wanted folders are selectable. Use --checkselectable to force this check.\n" ) ; + } + return ; +} + +sub setcheckselectable +{ + my $mysync = shift ; + + my $h1_folders_wanted_nb = scalar @{ $mysync->{ h1_folders_wanted } } ; + # 152 because 98% of host1 accounts have less than 152 folders on /X service. + # command to get this value: + # datamash_file_op_index G_Host1_Nb_folders.txt perc:98 4 %16.1f + if ( ! defined $mysync->{ checkselectable } ) + { + if ( 152 >= $h1_folders_wanted_nb ) + { + $mysync->{ checkselectable } = 1 ; + }else{ + myprint( "Host1: Not checking that $h1_folders_wanted_nb wanted folders are selectable. Use --checkselectable to force this check.\n" ) ; + $mysync->{ checkselectable } = 0 ; + } + } + return ; +} + + + +sub debugsleep +{ + my $mysync = shift @ARG ; + if ( defined $mysync->{debugsleep} ) { + myprint( "Info: sleeping $mysync->{debugsleep}s\n" ) ; + sleep $mysync->{debugsleep} ; + } + return ; +} + +sub tests_foldersize +{ + note( 'Entering tests_foldersize()' ) ; + + is( undef, foldersize( ), 'foldersize: no args => undef' ) ; + + + #is_deeply( {}, {}, 'foldersize: a hash is a hash' ) ; + #is_deeply( [], [], 'foldersize: an array is an array' ) ; + note( 'Leaving tests_foldersize()' ) ; + return ; +} + + + +# Globals: +# $fetch_hash_set +# +sub foldersize +{ + # size of one folder + my ( $mysync, $side, $imap, $search_cmd, $abletosearch, $folder ) = @ARG ; + + if ( ! all_defined( $mysync, $side, $imap, $folder ) ) + { + return ; + } + + # FTGate is RFC buggy with EXAMINE it does not act as SELECT + #if ( ! $imap->examine( $folder ) ) { + if ( ! $imap->select( $folder ) ) { + my $error = join q{}, + "$side Folder $folder: Could not select: ", + $imap->LastError, "\n" ; + errors_incr( $mysync, $error ) ; + return ; + } + + if ( $imap->IsUnconnected( ) ) + { + return ; + } + + my $hash_ref = { } ; + my @msgs = select_msgs( $imap, undef, $search_cmd, $abletosearch, $folder ) ; + my $nb_msgs = scalar @msgs ; + my $biggest_in_folder = 0 ; + @{ $hash_ref }{ @msgs } = ( undef ) if @msgs ; + + + my $stot = 0 ; + + if ( $imap->IsUnconnected( ) ) + { + return ; + } + + if ( $nb_msgs > 0 and @msgs ) { + if ( $abletosearch ) { + if ( ! $imap->fetch_hash( \@msgs, 'RFC822.SIZE', $hash_ref) ) { + my $error = "$side failure with fetch_hash: $EVAL_ERROR\n" ; + errors_incr( $mysync, $error ) ; + return ; + } + } + else + { + my $fetch_hash_uids = $fetch_hash_set || "1:*" ; + if ( ! $imap->fetch_hash( $fetch_hash_uids, 'RFC822.SIZE', $hash_ref ) ) { + my $error = "$side failure with fetch_hash: $EVAL_ERROR\n" ; + errors_incr( $mysync, $error ) ; + return ; + } + } + for ( keys %{ $hash_ref } ) { + my $size = $hash_ref->{ $_ }->{ 'RFC822.SIZE' } ; + if ( defined $size ) + { + $stot += $size ; + $biggest_in_folder = max( $biggest_in_folder, $size ) ; + } + } + } + return( $stot, $nb_msgs, $biggest_in_folder ) ; + +} + + +# The old subroutine that performed just one side at a time. +# Still here for a while, until confident with sub foldersize_diff_compute() +sub foldersizes +{ + my ( $mysync, $side, $imap, $search_cmd, $abletosearch, @folders ) = @_ ; + my $total_size = 0 ; + my $total_nb = 0 ; + my $biggest_in_all = 0 ; + + my $nb_folders = scalar @folders ; + my $ct_folders = 0 ; # folder counter. + myprint( "++++ Calculating sizes of $nb_folders folders on $side\n" ) ; + foreach my $folder ( @folders ) { + my $stot = 0 ; + my $nb_msgs = 0 ; + my $biggest_in_folder = 0 ; + + $ct_folders++ ; + myprintf( "$side folder %7s %-35s", "$ct_folders/$nb_folders", jux_utf8( $folder ) ) ; + if ( 'Host2' eq $side and not exists $mysync->{h2_folders_all_UPPER}{ uc $folder } ) { + myprint( " does not exist yet\n") ; + next ; + } + if ( 'Host1' eq $side and not exists $h1_folders_all{ $folder } ) { + myprint( " does not exist\n" ) ; + next ; + } + + last if $imap->IsUnconnected( ) ; + + ( $stot, $nb_msgs, $biggest_in_folder ) = foldersize( $mysync, $side, $imap, $search_cmd, $abletosearch, $folder ) ; + + myprintf( ' Size: %9s', $stot ) ; + myprintf( ' Messages: %5s', $nb_msgs ) ; + myprintf( " Biggest: %9s\n", $biggest_in_folder ) ; + $total_size += $stot ; + $total_nb += $nb_msgs ; + $biggest_in_all = max( $biggest_in_all, $biggest_in_folder ) ; + } + myprintf( "%s Nb folders: %11s folders\n", $side, $nb_folders ) ; + myprintf( "%s Nb messages: %11s messages\n", $side, $total_nb ) ; + myprintf( "%s Total size: %11s bytes (%s)\n", $side, $total_size, bytes_display_string_bin( $total_size ) ) ; + myprintf( "%s Biggest message: %11s bytes (%s)\n", $side, $biggest_in_all, bytes_display_string_bin( $biggest_in_all ) ) ; + myprintf( "%s Time spent on sizing: %11.1f seconds\n", $side, timenext( $mysync ) ) ; + return( $total_nb, $total_size ) ; +} + + +sub foldersize_diff_present +{ + my $mysync = shift ; + my $folder1 = shift ; + my $folder2 = shift ; + my $counter_str = shift ; + my $force = shift ; + + my $values1_str ; + my $values2_str ; + + if ( ! defined $mysync->{ folder1 }->{ $folder1 }->{ size } || $force ) + { + foldersize_diff_compute( $mysync, $folder1, $folder2, $force ) ; + } + + # again, but this time it means no availaible data. + if ( defined $mysync->{ folder1 }->{ $folder1 }->{ size } ) + { + $values1_str = sprintf( "Size: %9s Messages: %5s Biggest: %9s\n", + $mysync->{ folder1 }->{ $folder1 }->{ size }, + $mysync->{ folder1 }->{ $folder1 }->{ nb_msgs }, + $mysync->{ folder1 }->{ $folder1 }->{ biggest }, + ) ; + } + else + { + $values1_str = " does not exist\n" ; + } + + if ( defined $mysync->{ folder2 }->{ $folder2 }->{ size } ) + { + $values2_str = sprintf( "Size: %9s Messages: %5s Biggest: %9s\n", + $mysync->{ folder2 }->{ $folder2 }->{ size }, + $mysync->{ folder2 }->{ $folder2 }->{ nb_msgs }, + $mysync->{ folder2 }->{ $folder2 }->{ biggest }, + ) ; + } + else + { + $values2_str = " does not exist yet\n" ; + } + + myprintf( "Host1 folder %7s %-35s %s", + "$counter_str", + jux_utf8( $folder1 ), + $values1_str + ) ; + + myprintf( "Host2 folder %7s %-35s %s", + "$counter_str", + jux_utf8( $folder2 ), + $values2_str + ) ; + + myprintf( "Host2-Host1 %7s %-35s %9s %5s %9s\n\n", + "", + "", + $mysync->{ folder1 }->{ $folder1 }->{ size_diff }, + $mysync->{ folder1 }->{ $folder1 }->{ nb_msgs_diff }, + $mysync->{ folder1 }->{ $folder1 }->{ biggest_diff }, + + ) ; + + + + + return ; +} + +sub foldersize_diff_compute +{ + my $mysync = shift ; + my $folder1 = shift ; + my $folder2 = shift ; + my $force = shift ; + + + + my ( $size_1, $nb_msgs_1, $biggest_1 ) ; + # memoization + if ( + exists $h1_folders_all{ $folder1 } + && + ( + ! defined $mysync->{ folder1 }->{ $folder1 }->{ size } + || $force + ) + ) + { + #myprint( "foldersize folder1 $h1_folders_all{ $folder1 }\n" ) ; + ( $size_1, $nb_msgs_1, $biggest_1 ) = + foldersize( $mysync, + 'Host1', + $mysync->{ imap1 }, + $mysync->{ search1 }, + $mysync->{ abletosearch1 }, + $folder1 + ) ; + $mysync->{ folder1 }->{ $folder1 }->{ size } = $size_1 ; + $mysync->{ folder1 }->{ $folder1 }->{ nb_msgs } = $nb_msgs_1 ; + $mysync->{ folder1 }->{ $folder1 }->{ biggest } = $biggest_1 ; + } + else + { + $size_1 = $mysync->{ folder1 }->{ $folder1 }->{ size } ; + $nb_msgs_1 = $mysync->{ folder1 }->{ $folder1 }->{ nb_msgs } ; + $biggest_1 = $mysync->{ folder1 }->{ $folder1 }->{ biggest } ; + + } + + + my ( $size_2, $nb_msgs_2, $biggest_2 ) ; + if ( + exists $mysync->{ h2_folders_all_UPPER }{ uc $folder2 } + && + ( + ! defined $mysync->{ folder2 }->{ $folder2 }->{ size } + || $force + ) + ) + { + #myprint( "foldersize folder2\n" ) ; + ( $size_2, $nb_msgs_2, $biggest_2 ) = + foldersize( $mysync, + 'Host2', + $mysync->{ imap2 }, + $mysync->{ search2 }, + $mysync->{ abletosearch2 }, + $folder2 + ) ; + + $mysync->{ folder2 }->{ $folder2 }->{ size } = $size_2 ; + $mysync->{ folder2 }->{ $folder2 }->{ nb_msgs } = $nb_msgs_2 ; + $mysync->{ folder2 }->{ $folder2 }->{ biggest } = $biggest_2 ; + } + else + { + $size_2 = $mysync->{ folder2 }->{ $folder2 }->{ size } ; + $nb_msgs_2 = $mysync->{ folder2 }->{ $folder2 }->{ nb_msgs } ; + $biggest_2 = $mysync->{ folder2 }->{ $folder2 }->{ biggest } ; + + } + + + my $size_diff = diff( $size_2, $size_1 ) ; + my $nb_msgs_diff = diff( $nb_msgs_2, $nb_msgs_1 ) ; + my $biggest_diff = diff( $biggest_2, $biggest_1 ) ; + + $mysync->{ folder1 }->{ $folder1 }->{ size_diff } = $size_diff ; + $mysync->{ folder1 }->{ $folder1 }->{ nb_msgs_diff } = $nb_msgs_diff ; + $mysync->{ folder1 }->{ $folder1 }->{ biggest_diff } = $biggest_diff ; + + # It's redundant but easier to access later + $mysync->{ folder2 }->{ $folder2 }->{ size_diff } = $size_diff ; + $mysync->{ folder2 }->{ $folder2 }->{ nb_msgs_diff } = $nb_msgs_diff ; + $mysync->{ folder2 }->{ $folder2 }->{ biggest_diff } = $biggest_diff ; + + return ; +} + +sub diff +{ + my $x = shift ; + my $y = shift ; + + $x ||= 0 ; + $y ||= 0 ; + + return $x - $y ; +} + +sub add +{ + my $x = shift ; + my $y = shift ; + + $x ||= 0 ; + $y ||= 0 ; + + return $x + $y ; +} + +sub tests_checknoabletosearch +{ + note( 'Entering checknoabletosearch()' ) ; + + is( undef, checknoabletosearch( ), 'checknoabletosearch: no args => undef' ) ; + + note( 'Leaving checknoabletosearch()' ) ; + return ; +} + + + + +sub checknoabletosearch +{ + # call example: checknoabletosearch( $sync, $sync->{ imap1 }, 'INBOX', 'Host1' ) ; + # output: + # * undef if something is not ok to decide + # * 1 if SEARCH ALL failed + + my( $mysync, $imap, $folder, $HostX ) = @ARG ; + + if ( ! all_defined( $mysync, $imap, $folder, $HostX ) ) + { + return ; + } + + myprint( "$HostX: checking if SEARCH ALL works on $folder\n" ) ; + if ( ! select_folder( $mysync, $imap, $folder, $HostX ) ) + { + myprint( "$HostX: can not SELECT folder [$folder]\n" ) ; + return ; + } + my $count_from_select = count_from_select( $imap->History ) ; + myprint( "$HostX: folder [$folder] has $count_from_select messages mentioned by SELECT\n" ) ; + + my $msgs_all = $imap->messages( ) ; + if ( ! $msgs_all ) + { + myprint( "$HostX: can not SEARCH ALL folder [$folder]\n" ) ; + myprint( "$HostX: ", $imap->LastError(), "\n" ) ; + return 1 ; + } + + my $count_from_search_all = scalar( @{ $msgs_all } ) ; + myprint( "$HostX: folder [$folder] has $count_from_search_all messages found by SEARCH ALL\n" ) ; + + if ( $count_from_select == $count_from_search_all ) + { + myprint( "$HostX: folder [$folder] has the same messages count ($count_from_select) by SELECT and SEARCH ALL\n" ) ; + } + else + { + myprint( "$HostX: Warning, folder [$folder] has not the same count by SELECT ($count_from_select) and SEARCH ALL ($count_from_search_all)\n" ) ; + return 1 ; + } + + return ; +} + + +sub foldersizes_diff_list +{ + my $mysync = shift ; + my $force = shift ; + + my @folders = @{ $mysync->{h1_folders_wanted} } ; + my $nb_folders = scalar @folders ; + my $ct_folders = 0 ; # folder counter. + + foreach my $folder1 ( @folders ) + { + $ct_folders++ ; + my $counter_str = "$ct_folders/$nb_folders" ; + my $folder2 = imap2_folder_name( $mysync, $folder1 ) ; + foldersize_diff_present( $mysync, $folder1, $folder2, $counter_str, $force ) ; + } + + return ; +} + +sub foldersizes_total +{ + my $mysync = shift ; + + my @folders_1 = @{ $mysync->{h1_folders_wanted} } ; + my @folders_2 = @h2_folders_from_1_wanted ; + + my $nb_folders_1 = scalar( @folders_1 ) ; + my $nb_folders_2 = scalar( @folders_2 ) ; + + my ( $total_size_1, $total_nb_1, $biggest_in_all_1 ) = ( 0, 0, 0 ) ; + my ( $total_size_2, $total_nb_2, $biggest_in_all_2 ) = ( 0, 0, 0 ) ; + + foreach my $folder1 ( @folders_1 ) + { + $total_size_1 = add( $total_size_1, $mysync->{ folder1 }->{ $folder1 }->{ size } ) ; + $total_nb_1 = add( $total_nb_1, $mysync->{ folder1 }->{ $folder1 }->{ nb_msgs } ) ; + $biggest_in_all_1 = max( $biggest_in_all_1 , $mysync->{ folder1 }->{ $folder1 }->{ biggest } ) ; + } + + foreach my $folder2 ( @folders_2 ) + { + $total_size_2 = add( $total_size_2, $mysync->{ folder2 }->{ $folder2 }->{ size } ) ; + $total_nb_2 = add( $total_nb_2, $mysync->{ folder2 }->{ $folder2 }->{ nb_msgs } ) ; + $biggest_in_all_2 = max( $biggest_in_all_2 , $mysync->{ folder2 }->{ $folder2 }->{ biggest } ) ; + + } + + myprintf( "Host1 Nb folders: %11s folders\n", $nb_folders_1 ) ; + myprintf( "Host2 Nb folders: %11s folders\n", $nb_folders_2 ) ; + myprint( "\n" ) ; + myprintf( "Host1 Nb messages: %11s messages\n", $total_nb_1 ) ; + myprintf( "Host2 Nb messages: %11s messages\n", $total_nb_2 ) ; + myprint( "\n" ) ; + myprintf( "Host1 Total size: %11s bytes (%s)\n", $total_size_1, bytes_display_string_bin( $total_size_1 ) ) ; + myprintf( "Host2 Total size: %11s bytes (%s)\n", $total_size_2, bytes_display_string_bin( $total_size_2 ) ) ; + myprint( "\n" ) ; + myprintf( "Host1 Biggest message: %11s bytes (%s)\n", $biggest_in_all_1, bytes_display_string_bin( $biggest_in_all_1 ) ) ; + myprintf( "Host2 Biggest message: %11s bytes (%s)\n", $biggest_in_all_2, bytes_display_string_bin( $biggest_in_all_2 ) ) ; + myprint( "\n" ) ; + myprintf( "Time spent on sizing: %11.1f seconds\n", timenext( $mysync ) ) ; + + my @total_1_2 = ( $total_nb_1, $total_size_1, $total_nb_2, $total_size_2 ) ; + return @total_1_2 ; +} + +sub foldersizesatend_old +{ + my $mysync = shift ; + timenext( $mysync ) ; + return if ( $mysync->{imap1}->IsUnconnected( ) ) ; + return if ( $mysync->{imap2}->IsUnconnected( ) ) ; + # Get all folders on host2 again since new were created + @h2_folders_all = sort $mysync->{imap2}->folders(); + for ( @h2_folders_all ) { + $h2_folders_all{ $_ } = 1 ; + $mysync->{h2_folders_all_UPPER}{ uc $_ } = 1 ; + } ; + ( $h1_nb_msg_end, $h1_bytes_end ) = foldersizes( $mysync, 'Host1', $mysync->{imap1}, $mysync->{ search1 }, $mysync->{abletosearch1}, @{ $mysync->{h1_folders_wanted} } ) ; + ( $h2_nb_msg_end, $h2_bytes_end ) = foldersizes( $mysync, 'Host2', $mysync->{imap2}, $mysync->{ search2 }, $mysync->{abletosearch2}, @h2_folders_from_1_wanted ) ; + if ( not all_defined( $h1_nb_msg_end, $h1_bytes_end, $h2_nb_msg_end, $h2_bytes_end ) ) { + my $error = "Failure getting foldersizes, final differences will not be calculated\n" ; + errors_incr( $mysync, $error ) ; + } + return ; +} + +sub foldersizesatend +{ + my $mysync = shift ; + timenext( $mysync ) ; + return if ( $mysync->{imap1}->IsUnconnected( ) ) ; + return if ( $mysync->{imap2}->IsUnconnected( ) ) ; + # Get all folders on host2 again since new were created + @h2_folders_all = sort $mysync->{imap2}->folders(); + for ( @h2_folders_all ) { + $h2_folders_all{ $_ } = 1 ; + $mysync->{h2_folders_all_UPPER}{ uc $_ } = 1 ; + } ; + + + foldersizes_diff_list( $mysync, $FORCE ) ; + + ( $h1_nb_msg_end, $h1_bytes_end, $h2_nb_msg_end, $h2_bytes_end ) + = foldersizes_total( $mysync ) ; + + + if ( not all_defined( $h1_nb_msg_end, $h1_bytes_end, $h2_nb_msg_end, $h2_bytes_end ) ) { + my $error = "Failure getting foldersizes, final differences will not be calculated\n" ; + errors_incr( $mysync, $error ) ; + } + return ; +} + + +sub foldersizes_at_the_beggining +{ + my $mysync = shift ; + + myprint( << 'END_SIZE' ) ; + +Folders sizes before the synchronization. +You can remove foldersizes listings by using "--nofoldersizes" and "--nofoldersizesatend" +but then you will also lose the ETA (Estimation Time of Arrival) given after each message copy. +END_SIZE + + foldersizes_diff_list( $mysync ) ; + + ( $mysync->{ h1_nb_msg_start }, $mysync->{ h1_bytes_start }, + $mysync->{ h2_nb_msg_start }, $mysync->{ h2_bytes_start } ) + = foldersizes_total( $mysync ) ; + + + if ( not all_defined( + $mysync->{ h1_nb_msg_start }, + $mysync->{ h1_bytes_start }, + $mysync->{ h2_nb_msg_start }, + $mysync->{ h2_bytes_start } ) ) + { + my $error = "Failure getting foldersizes, ETA and final diff will not be displayed\n" ; + errors_incr( $mysync, $error ) ; + $mysync->{ foldersizes } = 0 ; + $mysync->{ foldersizesatend } = 0 ; + return ; + } + + my $h2_bytes_limit = $mysync->{ acc2 }->{quota_limit_bytes} || 0 ; + if ( $h2_bytes_limit and ( $h2_bytes_limit < $mysync->{ h1_bytes_start } ) ) + { + my $quota_percent = mysprintf( '%.0f', $NUMBER_100 * $mysync->{ h1_bytes_start } / $h2_bytes_limit ) ; + my $error = "Host2: Quota limit will be exceeded! Over $quota_percent % ( $mysync->{ h1_bytes_start } bytes / $h2_bytes_limit bytes )\n" ; + errors_incr( $mysync, $error ) ; + } + return ; +} + + +# Globals: +# @h2_folders_from_1_wanted + +sub foldersizes_at_the_beggining_old +{ + my $mysync = shift ; + + myprint( << 'END_SIZE' ) ; + +Folders sizes before the synchronization. +You can remove foldersizes listings by using "--nofoldersizes" and "--nofoldersizesatend" +but then you will also lose the ETA (Estimation Time of Arrival) given after each message copy. +END_SIZE + + ( $mysync->{ h1_nb_msg_start }, $mysync->{ h1_bytes_start } ) = + foldersizes( $mysync, 'Host1', $mysync->{imap1}, $mysync->{ search1 }, + $mysync->{abletosearch1}, @{ $mysync->{h1_folders_wanted} } ) ; + ( $mysync->{ h2_nb_msg_start }, $mysync->{ h2_bytes_start } ) = + foldersizes( $mysync, 'Host2', $mysync->{imap2}, $mysync->{ search2 }, + $mysync->{abletosearch2}, @h2_folders_from_1_wanted ) ; + + if ( not all_defined( $mysync->{ h1_nb_msg_start }, + $mysync->{ h1_bytes_start }, $mysync->{ h2_nb_msg_start }, $mysync->{ h2_bytes_start } ) ) + { + my $error = "Failure getting foldersizes, ETA and final diff will not be displayed\n" ; + errors_incr( $mysync, $error ) ; + $mysync->{ foldersizes } = 0 ; + $mysync->{ foldersizesatend } = 0 ; + return ; + } + + my $h2_bytes_limit = $mysync->{ acc2 }->{quota_limit_bytes} || 0 ; + if ( $h2_bytes_limit and ( $h2_bytes_limit < $mysync->{ h1_bytes_start } ) ) + { + my $quota_percent = mysprintf( '%.0f', $NUMBER_100 * $mysync->{ h1_bytes_start } / $h2_bytes_limit ) ; + my $error = "Host2: Quota limit will be exceeded! Over $quota_percent % ( $mysync->{ h1_bytes_start } bytes / $h2_bytes_limit bytes )\n" ; + errors_incr( $mysync, $error ) ; + } + return ; +} + + +sub tests_total_bytes_max_reached +{ + note( 'Entering tests_total_bytes_max_reached()' ) ; + + is( undef, total_bytes_max_reached( ), 'total_bytes_max_reached: no args => undef' ) ; + + my $mysync = {} ; + is( undef, total_bytes_max_reached( $mysync ), 'total_bytes_max_reached: no exitwhenover => undef' ) ; + + $mysync->{ exitwhenover } = 300 ; + is( undef, total_bytes_max_reached( $mysync ), 'total_bytes_max_reached: exitwhenover 300 but no total_bytes_transferred => undef' ) ; + + $mysync->{ total_bytes_transferred } = 200 ; + is( undef, total_bytes_max_reached( $mysync ), 'total_bytes_max_reached: exitwhenover 300 but total_bytes_transferred 200 => undef' ) ; + + $mysync->{ total_bytes_transferred } = 400 ; + is( 1, total_bytes_max_reached( $mysync ), 'total_bytes_max_reached: exitwhenover 300 but total_bytes_transferred 400 => 1' ) ; + + + + note( 'Leaving tests_total_bytes_max_reached()' ) ; + return ; +} + + +sub total_bytes_max_reached +{ + my $mysync = shift ; + + if ( ! defined $mysync ) { return ; } + + if ( ! $mysync->{ exitwhenover } ) + { + return ; + } + + if ( ! $mysync->{ total_bytes_transferred } ) + { + return ; + } + + if ( $mysync->{ total_bytes_transferred } >= $mysync->{ exitwhenover } ) + { + my $error = "Maximum bytes transferred reached, $mysync->{total_bytes_transferred} >= $mysync->{ exitwhenover }, ending sync\n" ; + errors_incr( $mysync, $error ) ; + return( 1 ) ; + } + return ; +} + + +sub tests_mock_capability +{ + note( 'Entering tests_mock_capability()' ) ; + + my $myimap ; + ok( $myimap = mock_capability( ), + 'mock_capability: (1) no args => a Test::MockObject' + ) ; + ok( $myimap->isa( 'Test::MockObject' ), + 'mock_capability: (2) no args => a Test::MockObject' + ) ; + + is( undef, $myimap->capability( ), + 'mock_capability: (3) no args => capability undef' + ) ; + + ok( mock_capability( $myimap ), + 'mock_capability: (1) one arg => MockObject' + ) ; + + is( undef, $myimap->capability( ), + 'mock_capability: (2) one arg OO style => capability undef' + ) ; + + ok( mock_capability( $myimap, $NUMBER_123456 ), + 'mock_capability: (1) two args 123456 => capability 123456' + ) ; + + is( $NUMBER_123456, $myimap->capability( ), + 'mock_capability: (2) two args 123456 => capability 123456' + ) ; + + ok( mock_capability( $myimap, 'ABCD' ), + 'mock_capability: (1) two args ABCD => capability ABCD' + ) ; + is( 'ABCD', $myimap->capability( ), + 'mock_capability: (2) two args ABCD => capability ABCD' + ) ; + + ok( mock_capability( $myimap, [ 'ABCD' ] ), + 'mock_capability: (1) two args [ ABCD ] => capability [ ABCD ]' + ) ; + is_deeply( [ 'ABCD' ], $myimap->capability( ), + 'mock_capability: (2) two args [ ABCD ] => capability [ ABCD ]' + ) ; + + ok( mock_capability( $myimap, [ 'ABC', 'DEF' ] ), + 'mock_capability: (1) two args [ ABC, DEF ] => capability [ ABC, DEF ]' + ) ; + is_deeply( [ 'ABC', 'DEF' ], $myimap->capability( ), + 'mock_capability: (2) two args [ ABC, DEF ] => capability capability [ ABC, DEF ]' + ) ; + + ok( mock_capability( $myimap, 'ABC', 'DEF' ), + 'mock_capability: (1) two args ABC, DEF => capability [ ABC, DEF ]' + ) ; + is_deeply( [ 'ABC', 'DEF' ], [ $myimap->capability( ) ], + 'mock_capability: (2) two args ABC, DEF => capability capability [ ABC, DEF ]' + ) ; + + ok( mock_capability( $myimap, 'IMAP4rev1', 'APPENDLIMIT=123456' ), + 'mock_capability: (1) two args IMAP4rev1, APPENDLIMIT=123456 => capability [ IMAP4rev1, APPENDLIMIT=123456 ]' + ) ; + is_deeply( [ 'IMAP4rev1', 'APPENDLIMIT=123456' ], [ $myimap->capability( ) ], + 'mock_capability: (2) two args IMAP4rev1, APPENDLIMIT=123456 => capability capability [ IMAP4rev1, APPENDLIMIT=123456 ]' + ) ; + + note( 'Leaving tests_mock_capability()' ) ; + return ; +} + +sub sig_install_toggle_sleep +{ + my $mysync = shift ; + if ( 'MSWin32' ne $OSNAME ) { + #myprint( "sig_install( $mysync, \&toggle_sleep, 'USR1' )\n" ) ; + sig_install( $mysync, 'toggle_sleep', 'USR1' ) ; + } + #myprint( "Leaving sig_install_toggle_sleep\n" ) ; + return ; +} + + +sub mock_capability +{ + my $myimap = shift ; + my @has_capability_value = @ARG ; + my ( $has_capability_value ) = @has_capability_value ; + + if ( ! $myimap ) + { + require_ok( "Test::MockObject" ) ; + $myimap = Test::MockObject->new( ) ; + } + + $myimap->mock( + 'capability', + sub { return wantarray ? + @has_capability_value + : $has_capability_value ; + } + ) ; + + return $myimap ; +} + + +sub tests_capability_of +{ + note( 'Entering tests_capability_of()' ) ; + + is( undef, capability_of( ), + 'capability_of: no args => undef' ) ; + + my $myimap ; + is( undef, capability_of( $myimap ), + 'capability_of: undef => undef' ) ; + + + $myimap = mock_capability( $myimap, 'IMAP4rev1', 'APPENDLIMIT=123456' ) ; + + is( undef, capability_of( $myimap, 'CACA' ), + 'capability_of: two args unknown capability => undef' ) ; + + + is( $NUMBER_123456, capability_of( $myimap, 'APPENDLIMIT' ), + 'capability_of: two args APPENDLIMIT 123456 => 123456 yeah!' ) ; + + note( 'Leaving tests_capability_of()' ) ; + return ; +} + + +sub capability_of +{ + my $imap = shift || return ; + my $capability_keyword = shift || return ; + + my @capability = $imap->capability ; + + if ( ! @capability ) { return ; } + my $capability_value = search_in_array( $capability_keyword, @capability ) ; + + return $capability_value ; +} + + +sub tests_search_in_array +{ + note( 'Entering tests_search_in_array()' ) ; + + is( undef, search_in_array( 'KA' ), + 'search_in_array: no array => undef ' ) ; + + is( 'VA', search_in_array( 'KA', ( 'KA=VA' ) ), + 'search_in_array: KA KA=VA => VA ' ) ; + + is( 'VA', search_in_array( 'KA', ( 'KA=VA', 'KB=VB' ) ), + 'search_in_array: KA KA=VA KB=VB => VA ' ) ; + + is( 'VB', search_in_array( 'KB', ( 'KA=VA', 'KB=VB' ) ), + 'search_in_array: KA=VA KB=VB => VB ' ) ; + + note( 'Leaving tests_search_in_array()' ) ; + return ; +} + +sub search_in_array +{ + my ( $key, @array ) = @ARG ; + + foreach my $item ( @array ) + { + + if ( $item =~ /([^=]+)=(.*)/ ) + { + if ( $1 eq $key ) + { + return $2 ; + } + } + } + + return ; +} + + + + +sub tests_appendlimit_from_capability +{ + note( 'Entering tests_appendlimit_from_capability()' ) ; + + is( undef, appendlimit_from_capability( ), + 'appendlimit_from_capability: no args => undef' + ) ; + + my $myimap ; + is( undef, appendlimit_from_capability( $myimap ), + 'appendlimit_from_capability: undef arg => undef' + ) ; + + + $myimap = mock_capability( $myimap, 'IMAP4rev1', 'APPENDLIMIT=123456' ) ; + + # Normal behavior + is( $NUMBER_123456, appendlimit_from_capability( $myimap ), + 'appendlimit_from_capability: APPENDLIMIT=123456 => 123456' + ) ; + + # Not a number + $myimap = mock_capability( $myimap, 'IMAP4rev1', 'APPENDLIMIT=ABC' ) ; + + is( undef, appendlimit_from_capability( $myimap ), + 'appendlimit_from_capability: not a number => undef' + ) ; + + note( 'Leaving tests_appendlimit_from_capability()' ) ; + return ; +} + + +sub appendlimit_from_capability +{ + my $myimap = shift ; + if ( ! $myimap ) + { + myprint( "Warn: no imap with call to appendlimit_from_capability\n" ) ; + return ; + } + + #myprint( Data::Dumper->Dump( [ \$myimap ] ) ) ; + my $appendlimit = capability_of( $myimap, 'APPENDLIMIT' ) ; + #myprint( "has_capability APPENDLIMIT $appendlimit\n" ) ; + if ( is_integer( $appendlimit ) ) + { + return $appendlimit ; + } + return ; +} + + +sub tests_appendlimit +{ + note( 'Entering tests_appendlimit()' ) ; + + is( undef, appendlimit( ), + 'appendlimit: no args => undef' + ) ; + + my $mysync = { } ; + + is( undef, appendlimit( $mysync ), + 'appendlimit: no imap2 => undef' + ) ; + + my $myimap ; + $myimap = mock_capability( $myimap, 'IMAP4rev1', 'APPENDLIMIT=123456' ) ; + + $mysync->{ imap2 } = $myimap ; + + is( 123456, appendlimit( $mysync ), + 'appendlimit: imap2 with APPENDLIMIT=123456 => 123456' + ) ; + + note( 'Leaving tests_appendlimit()' ) ; + return ; +} + +sub appendlimit +{ + my $mysync = shift || return ; + my $myimap = $mysync->{ imap2 } ; + + my $appendlimit = appendlimit_from_capability( $myimap ) ; + if ( defined $appendlimit ) + { + myprint( "Host2: found APPENDLIMIT=$appendlimit in CAPABILITY (use --appendlimit xxxx to override this automatic setting)\n" ) ; + return $appendlimit ; + } + return ; + +} + + +sub tests_maxsize_setting +{ + note( 'Entering tests_maxsize_setting()' ) ; + + is( undef, maxsize_setting( ), + 'maxsize_setting: no args => undef' + ) ; + + my $mysync ; + + is( undef, maxsize_setting( $mysync ), + 'maxsize_setting: undef arg => undef' + ) ; + + $mysync = { } ; + $mysync->{ maxsize } = $NUMBER_123456 ; + + # --maxsize alone + is( $NUMBER_123456, maxsize_setting( $mysync ), + 'maxsize_setting: --maxsize 123456 alone => 123456' + ) ; + + + $mysync = { } ; + my $myimap ; + + $myimap = mock_capability( $myimap, 'IMAP4rev1', 'APPENDLIMIT=654321' ) ; + $mysync->{ imap2 } = $myimap ; + + # APPENDLIMIT alone + is( $NUMBER_654321, maxsize_setting( $mysync ), + 'maxsize_setting: APPENDLIMIT 654321 alone => 654321' + ) ; + + is( $NUMBER_654321, $mysync->{ maxsize }, + 'maxsize_setting: APPENDLIMIT 654321 alone => maxsize 654321' + ) ; + + # APPENDLIMIT with --appendlimit => --appendlimit wins + $mysync->{ appendlimit } = $NUMBER_123456 ; + + is( $NUMBER_123456, maxsize_setting( $mysync ), + 'maxsize_setting: APPENDLIMIT 654321 + --appendlimit 123456 => 123456' + ) ; + + is( $NUMBER_123456, $mysync->{ maxsize }, + 'maxsize_setting: APPENDLIMIT 654321 + --appendlimit 123456 => maxsize 123456' + ) ; + + # Fresh + $mysync = { } ; + $mysync->{ imap2 } = $myimap = mock_capability( $myimap, 'IMAP4rev1', 'APPENDLIMIT=654321' ) ; + + # Case: "APPENDLIMIT >= --maxsize" => maxsize. + $mysync->{ maxsize } = $NUMBER_123456 ; + + is( $NUMBER_123456, maxsize_setting( $mysync ), + 'maxsize_setting: APPENDLIMIT 654321 --maxsize 123456 => 123456' + ) ; + + # Case: "APPENDLIMIT < --maxsize" => APPENDLIMIT. + + + # Fresh + $mysync = { } ; + $mysync->{ imap2 } = $myimap = mock_capability( $myimap, 'IMAP4rev1', 'APPENDLIMIT=123456' ) ; + $mysync->{ maxsize } = $NUMBER_654321 ; + + is( $NUMBER_123456, maxsize_setting( $mysync ), + 'maxsize_setting: APPENDLIMIT 123456 --maxsize 654321 => 123456 ' + ) ; + + # Now --truncmess stuff + + note( 'Leaving tests_maxsize_setting()' ) ; + + return ; +} + +# Three variables to take account of +# appendlimit (given by --appendlimit or CAPABILITY...) +# maxsize +# truncmess + +sub maxsize_setting +{ + my $mysync = shift || return ; + + if ( defined $mysync->{ appendlimit } ) + { + myprint( "Host2: Getting appendlimit from --appendlimit $mysync->{ appendlimit }\n" ) ; + } + else + { + $mysync->{ appendlimit } = appendlimit( $mysync ) ; + } + + + if ( all_defined( $mysync->{ appendlimit }, $mysync->{ maxsize } ) ) + { + my $min_maxsize_appendlimit = min( $mysync->{ maxsize }, $mysync->{ appendlimit } ) ; + myprint( "Host2: Setting maxsize to $min_maxsize_appendlimit (min of --maxsize $mysync->{ maxsize } and appendlimit $mysync->{ appendlimit }\n" ) ; + $mysync->{ maxsize } = $min_maxsize_appendlimit ; + return $mysync->{ maxsize } ; + } + elsif ( defined $mysync->{ appendlimit } ) + { + myprint( "Host2: Setting maxsize to appendlimit $mysync->{ appendlimit }\n" ) ; + $mysync->{ maxsize } = $mysync->{ appendlimit } ; + return $mysync->{ maxsize } ; + }elsif ( defined $mysync->{ maxsize } ) + { + return $mysync->{ maxsize } ; + }else + { + return ; + } +} + + + + +sub all_defined +{ + if ( not @ARG ) { + return 0 ; + } + foreach my $elem ( @ARG ) { + if ( not defined $elem ) { + return 0 ; + } + } + return 1 ; +} + +sub tests_all_defined +{ + note( 'Entering tests_all_defined()' ) ; + + is( 0, all_defined( ), 'all_defined: no param => 0' ) ; + is( 0, all_defined( () ), 'all_defined: void list => 0' ) ; + is( 0, all_defined( undef ), 'all_defined: undef => 0' ) ; + is( 0, all_defined( undef, undef ), 'all_defined: undef => 0' ) ; + is( 0, all_defined( 1, undef ), 'all_defined: 1 undef => 0' ) ; + is( 0, all_defined( undef, 1 ), 'all_defined: undef 1 => 0' ) ; + is( 1, all_defined( 1, 1 ), 'all_defined: 1 1 => 1' ) ; + is( 1, all_defined( (1, 1) ), 'all_defined: (1 1) => 1' ) ; + + note( 'Leaving tests_all_defined()' ) ; + return ; +} + + +sub tests_hashsynclocal +{ + note( 'Entering tests_hashsynclocal()' ) ; + + my $mysync = { + host1 => q{}, + user1 => q{}, + password1 => q{}, + host2 => q{}, + user2 => q{}, + password2 => q{}, + } ; + + is( undef, hashsynclocal( $mysync ), 'hashsynclocal: no hashfile name' ) ; + + $mysync->{ hashfile } = q{} ; + is( undef, hashsynclocal( $mysync ), 'hashsynclocal: empty hashfile name' ) ; + + $mysync->{ hashfile } = './noexist/rrr' ; + is( undef, hashsynclocal( $mysync ), 'hashsynclocal: no exists hashfile dir' ) ; + + SKIP: { + if ( 'MSWin32' eq $OSNAME or '0' eq $EFFECTIVE_USER_ID ) { skip( 'Tests only for non-root Unix', 1 ) ; } + $mysync->{ hashfile } = '/rrr' ; + is( undef, hashsynclocal( $mysync ), 'hashsynclocal: permission denied' ) ; + } + ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'hashsynclocal: mkpath W/tmp/tests/' ) ; + $mysync->{ hashfile } = 'W/tmp/tests/imapsync_hash' ; + + ok( ! -e 'W/tmp/tests/imapsync_hash' || unlink 'W/tmp/tests/imapsync_hash', 'hashsynclocal: unlink W/tmp/tests/imapsync_hash' ) ; + ok( ! -e 'W/tmp/tests/imapsync_hash', 'hashsynclocal: verify there is no W/tmp/tests/imapsync_hash' ) ; + is( 'ecdeb4ede672794d173da4e08c52b8ee19b7d252', hashsynclocal( $mysync, 'mukksyhpmbixkxkpjlqivmlqsulpictj' ), 'hashsynclocal: creating/reading W/tmp/tests/imapsync_hash' ) ; + # A second time now + is( 'ecdeb4ede672794d173da4e08c52b8ee19b7d252', hashsynclocal( $mysync ), 'hashsynclocal: reading W/tmp/tests/imapsync_hash second time => same' ) ; + + note( 'Leaving tests_hashsynclocal()' ) ; + return ; +} + +sub hashsynclocal +{ + my $mysync = shift ; + my $hashkey = shift ; # Optional, only there for tests + my $hashfile = $mysync->{ hashfile } ; + $hashfile = createhashfileifneeded( $hashfile, $hashkey ) ; + if ( ! $hashfile ) { + return ; + } + $hashkey = firstline( $hashfile ) ; + if ( ! $hashkey ) { + myprint( "No hashkey!\n" ) ; + return ; + } + my $hashsynclocal = hashsync( $mysync, $hashkey ) ; + return( $hashsynclocal ) ; + +} + +sub tests_hashsync +{ + note( 'Entering tests_hashsync()' ) ; + + is( 'fbdb1d1b18aa6c08324b7d64b71fb76370690e1d', hashsync( ), 'hashsync: no args' ) ; + + is( 'fbdb1d1b18aa6c08324b7d64b71fb76370690e1d', hashsync( {}, q{} ), 'hashsync: empty args' ) ; + my $mysync ; + $mysync->{ host1 } = 'zzz' ; + is( 'e86a28a3611c1e7bbaf8057cd00ae122781a11fe', hashsync( $mysync, q{} ), 'hashsync: host1 zzz => ' ) ; + is( '6a7b451ac99eab1531ad8e6cd544b32420c552ac', hashsync( $mysync, q{A} ), 'hashsync: host1 zzz => ' ) ; + $mysync->{ host2 } = 'zzz' ; + is( '15959573e4a86763253a7aedb1a2b0c60d133dc2', hashsync( $mysync, q{} ), 'hashsync: + host2 zzz => ' ) ; + is( 'b8d4ab541b209c75928528020ca28ee43488bd8f', hashsync( $mysync, 'A' ), 'hashsync: + hashkey A => ' ) ; + + $mysync = undef ; + is( 'fbdb1d1b18aa6c08324b7d64b71fb76370690e1d', hashsync( $mysync, q{} ), 'hashsync: undef $mysync' ) ; + $mysync->{ password1 } = 'abcd' ; + is( 'afa29ab8534495251ac8346a985717c54bc49c26', hashsync( $mysync, q{} ), 'hashsync: password1: abcd' ) ; + + # A user reported a massive failure on /X (Thomas V. 21/04/2020 à 21:41 Subject: Error) + # "Wide character in subroutine entry at /usr/local/lib/perl5/site_perl/Digest/HMAC.pm" + # I can reproduce it now + + + # The eval is there to avoid a complete crash + # this one is fatal so it is commented + # is( 'f1a3f3dac3f137fd658027c11678b895f773ce55', 1 / 0 , 'hashsync: 1 / 0 fatal' ) ; + + my $eval ; + # this one is not fatal + is( undef, $eval = eval { 1 / 0 } , 'hashsync: 1/0 not fatal' ) ; + # this one neither + $mysync->{ password1 } = 'Ö' ; + is( 'f1a3f3dac3f137fd658027c11678b895f773ce55', $eval = eval { hashsync( $mysync, q{} ) } , 'hashsync: password1: Ö with eval' ) ; + + $mysync->{ password1 } = 'Ö' ; + is( 'f1a3f3dac3f137fd658027c11678b895f773ce55', hashsync( $mysync, q{} ), 'hashsync: password1: Ö without eval' ) ; + + $mysync->{ password1 } = qq{\x{00D6}} ; + is( 'bb5bfb461e79ecd3dbc6ade2aabb52d22fa8be1a', $eval = eval { hashsync( $mysync, q{} ) }, 'hashsync: password1: \x{00D6}' ) ; # + + print qq{1 00D6:Ö\n} ; + print encode_utf8( qq{2 00D6:Ö\n} ) ; + print qq{3 00D6:\x{00D6}\n} ; + print encode_utf8( qq{4 00D6:\x{00D6}\n} ) ; + + + print qq{5 6536:æâ€Â¶\n} ; + print encode_utf8( qq{6 6536:æâ€Â¶\n} ) ; + # the next one prints "Wide character in print at ./imapsync line xxxx" + print qq{7 6536:\x{6536}\n} ; + print encode_utf8( qq{8 6536:\x{6536}\n} ) ; + + $mysync->{ password1 } = qq{æâ€Â¶} ; + is( '4199f02773d1cd5599b1a8f2d024bdceb8b48e0b', hashsync( $mysync, q{} ), 'hashsync: password1: æâ€Â¶' ) ; + + $mysync->{ password1 } = qq{\x{6536}} ; + is( '4199f02773d1cd5599b1a8f2d024bdceb8b48e0b', $eval = eval{ hashsync( $mysync, q{} ) }, 'hashsync: password1: \x{6536} with eval' ) ; + + # No side effect. + $mysync->{ password1 } = 'abcd' ; + is( 'afa29ab8534495251ac8346a985717c54bc49c26', hashsync( $mysync, q{} ), 'hashsync: password1: abcd again' ) ; + + note( 'Leaving tests_hashsync()' ) ; + return ; +} + +sub hashsync +{ + my $mysync = shift ; + my $hashkey = shift ; + + my $mystring = join( q{}, + $mysync->{ host1 } || q{}, + $mysync->{ user1 } || q{}, + $mysync->{ password1 } || q{}, + $mysync->{ host2 } || q{}, + $mysync->{ user2 } || q{}, + $mysync->{ password2 } || q{}, + ) ; + #my $hashsync = hmac_sha1_hex( $mystring, $hashkey ) ; + my $hashsync = hmac_sha1_hex_robust( $mystring, $hashkey ) ; + #myprint( "$hashsync\n" ) ; + return( $hashsync ) ; +} + + +sub tests_hmac_sha1_hex +{ + note( 'Entering tests_hmac_sha1_hex()' ) ; + + is( 'fbdb1d1b18aa6c08324b7d64b71fb76370690e1d', hmac_sha1_hex( ), 'hmac_sha1_hex: no args => fbdb1d1b18aa6c08324b7d64b71fb76370690e1d' ) ; + is( 'fbdb1d1b18aa6c08324b7d64b71fb76370690e1d', hmac_sha1_hex( '' ), 'hmac_sha1_hex: empty string => fbdb1d1b18aa6c08324b7d64b71fb76370690e1d' ) ; + is( 'fbdb1d1b18aa6c08324b7d64b71fb76370690e1d', hmac_sha1_hex( '', '' ), 'hmac_sha1_hex: empty strings => fbdb1d1b18aa6c08324b7d64b71fb76370690e1d' ) ; + is( 'fbdb1d1b18aa6c08324b7d64b71fb76370690e1d', hmac_sha1_hex( '', '', 'caca' ), 'hmac_sha1_hex: empty strings + caca => fbdb1d1b18aa6c08324b7d64b71fb76370690e1d' ) ; + + # Good + is( 'f1a3f3dac3f137fd658027c11678b895f773ce55', hmac_sha1_hex( 'Ö' ), 'hmac_sha1_hex: Ö => f1a3f3dac3f137fd658027c11678b895f773ce55' ) ; + is( 'f1a3f3dac3f137fd658027c11678b895f773ce55', hmac_sha1_hex( encode_utf8(qq{\x{00D6}}) ), 'hmac_sha1_hex: encode_utf8 \x{00D6} => f1a3f3dac3f137fd658027c11678b895f773ce55' ) ; + # Bad + is( 'fe8dc3b9ba3e8850bb4a7b070b2279e911003af2', hmac_sha1_hex( encode_utf8( 'Ö' ) ), 'hmac_sha1_hex: encode_utf8 Ö => fe8dc3b9ba3e8850bb4a7b070b2279e911003af2' ) ; + is( 'bb5bfb461e79ecd3dbc6ade2aabb52d22fa8be1a', hmac_sha1_hex( qq{\x{00D6}} ), 'hmac_sha1_hex: qq{\x{00D6}} => bb5bfb461e79ecd3dbc6ade2aabb52d22fa8be1a' ) ; + + # Good + is( 'a6fda2a6acdd74630b20aac0c68716048ecd0333', hmac_sha1_hex( 'A' ), 'hmac_sha1_hex: A => a6fda2a6acdd74630b20aac0c68716048ecd0333' ) ; + is( 'a6fda2a6acdd74630b20aac0c68716048ecd0333', hmac_sha1_hex( encode_utf8(qq{\x{0041}}) ), 'hmac_sha1_hex: encode_utf8 \x{0041} => a6fda2a6acdd74630b20aac0c68716048ecd0333' ) ; + is( 'a6fda2a6acdd74630b20aac0c68716048ecd0333', hmac_sha1_hex( encode_utf8( 'A' ) ), 'hmac_sha1_hex: encode_utf8 A => a6fda2a6acdd74630b20aac0c68716048ecd0333' ) ; + is( 'a6fda2a6acdd74630b20aac0c68716048ecd0333', hmac_sha1_hex( qq{\x{0041}} ), 'hmac_sha1_hex: \x{0041} => a6fda2a6acdd74630b20aac0c68716048ecd0333' ) ; + + # Good + is( '36c54f255b575a2db58921d116b37c8af94c08cd', hmac_sha1_hex( 'A', 'B' ), 'hmac_sha1_hex: A B => 36c54f255b575a2db58921d116b37c8af94c08cd' ) ; + is( '36c54f255b575a2db58921d116b37c8af94c08cd', hmac_sha1_hex( encode_utf8(qq{\x{0041}}), 'B' ), 'hmac_sha1_hex: encode_utf8 \x{0041} B => 36c54f255b575a2db58921d116b37c8af94c08cd' ) ; + is( '36c54f255b575a2db58921d116b37c8af94c08cd', hmac_sha1_hex( encode_utf8( 'A' ), 'B' ), 'hmac_sha1_hex: encode_utf8 A B => 36c54f255b575a2db58921d116b37c8af94c08cd' ) ; + is( '36c54f255b575a2db58921d116b37c8af94c08cd', hmac_sha1_hex( qq{\x{0041}}, 'B' ), 'hmac_sha1_hex: \x{0041} B => 36c54f255b575a2db58921d116b37c8af94c08cd' ) ; + + # http://unicode.scarfboy.com/?s=U%2B6536 + # Good + is( '4199f02773d1cd5599b1a8f2d024bdceb8b48e0b', hmac_sha1_hex( 'æâ€Â¶' ), 'hmac_sha1_hex: æâ€Â¶ => 4199f02773d1cd5599b1a8f2d024bdceb8b48e0b' ) ; + is( '4199f02773d1cd5599b1a8f2d024bdceb8b48e0b', hmac_sha1_hex( encode_utf8(qq{\x{6536}}) ), 'hmac_sha1_hex: encode_utf8 \x{6536} => 4199f02773d1cd5599b1a8f2d024bdceb8b48e0b' ) ; + # Bad + is( 'e82217119628ad03e659cc89671d05ea4cee7238', hmac_sha1_hex( encode_utf8( 'æâ€Â¶' ) ), 'hmac_sha1_hex: encode_utf8 æâ€Â¶ => e82217119628ad03e659cc89671d05ea4cee7238' ) ; + # Very very bad, perl dies... + #is( '4199f02773d1cd5599b1a8f2d024bdceb8b48e0b', hmac_sha1_hex( qq{\x{6536}} ), 'hmac_sha1_hex: \x{6536} => 4199f02773d1cd5599b1a8f2d024bdceb8b48e0b' ) ; + # Ok but well, bad indeed + is( undef, my $eval = eval{ hmac_sha1_hex( qq{\x{6536}} ) }, 'hmac_sha1_hex: \x{6536} => undef' ) ; + + + note( 'Leaving tests_hmac_sha1_hex()' ) ; + return ; +} + +sub tests_hmac_sha1_hex_robust +{ + note( 'Entering tests_hmac_sha1_hex_robust()' ) ; + + is( 'fbdb1d1b18aa6c08324b7d64b71fb76370690e1d', hmac_sha1_hex_robust( ), 'hmac_sha1_hex_robust: no args => fbdb1d1b18aa6c08324b7d64b71fb76370690e1d' ) ; + is( 'fbdb1d1b18aa6c08324b7d64b71fb76370690e1d', hmac_sha1_hex_robust( '' ), 'hmac_sha1_hex_robust: empty string => fbdb1d1b18aa6c08324b7d64b71fb76370690e1d' ) ; + is( 'fbdb1d1b18aa6c08324b7d64b71fb76370690e1d', hmac_sha1_hex_robust( '', '' ), 'hmac_sha1_hex_robust: empty strings => fbdb1d1b18aa6c08324b7d64b71fb76370690e1d' ) ; + is( 'fbdb1d1b18aa6c08324b7d64b71fb76370690e1d', hmac_sha1_hex_robust( '', '', 'caca' ), 'hmac_sha1_hex_robust: empty strings + caca => fbdb1d1b18aa6c08324b7d64b71fb76370690e1d' ) ; + + # Good + is( 'f1a3f3dac3f137fd658027c11678b895f773ce55', hmac_sha1_hex_robust( 'Ö' ), 'hmac_sha1_hex_robust: Ö => f1a3f3dac3f137fd658027c11678b895f773ce55' ) ; + is( 'f1a3f3dac3f137fd658027c11678b895f773ce55', hmac_sha1_hex_robust( encode_utf8(qq{\x{00D6}}) ), 'hmac_sha1_hex_robust: encode_utf8 \x{00D6} => f1a3f3dac3f137fd658027c11678b895f773ce55' ) ; + # Bad + is( 'fe8dc3b9ba3e8850bb4a7b070b2279e911003af2', hmac_sha1_hex_robust( encode_utf8( 'Ö' ) ), 'hmac_sha1_hex_robust: encode_utf8 Ö => fe8dc3b9ba3e8850bb4a7b070b2279e911003af2' ) ; + is( 'bb5bfb461e79ecd3dbc6ade2aabb52d22fa8be1a', hmac_sha1_hex_robust( qq{\x{00D6}} ), 'hmac_sha1_hex_robust: qq{\x{00D6}} => bb5bfb461e79ecd3dbc6ade2aabb52d22fa8be1a' ) ; + + # Good + is( 'a6fda2a6acdd74630b20aac0c68716048ecd0333', hmac_sha1_hex_robust( 'A' ), 'hmac_sha1_hex_robust: A => a6fda2a6acdd74630b20aac0c68716048ecd0333' ) ; + is( 'a6fda2a6acdd74630b20aac0c68716048ecd0333', hmac_sha1_hex_robust( encode_utf8(qq{\x{0041}}) ), 'hmac_sha1_hex_robust: encode_utf8 \x{0041} => a6fda2a6acdd74630b20aac0c68716048ecd0333' ) ; + is( 'a6fda2a6acdd74630b20aac0c68716048ecd0333', hmac_sha1_hex_robust( encode_utf8( 'A' ) ), 'hmac_sha1_hex_robust: encode_utf8 A => a6fda2a6acdd74630b20aac0c68716048ecd0333' ) ; + is( 'a6fda2a6acdd74630b20aac0c68716048ecd0333', hmac_sha1_hex_robust( qq{\x{0041}} ), 'hmac_sha1_hex_robust: \x{0041} => a6fda2a6acdd74630b20aac0c68716048ecd0333' ) ; + + # Good + is( '36c54f255b575a2db58921d116b37c8af94c08cd', hmac_sha1_hex_robust( 'A', 'B' ), 'hmac_sha1_hex_robust: A B => 36c54f255b575a2db58921d116b37c8af94c08cd' ) ; + is( '36c54f255b575a2db58921d116b37c8af94c08cd', hmac_sha1_hex_robust( encode_utf8(qq{\x{0041}}), 'B' ), 'hmac_sha1_hex_robust: encode_utf8 \x{0041} B => 36c54f255b575a2db58921d116b37c8af94c08cd' ) ; + is( '36c54f255b575a2db58921d116b37c8af94c08cd', hmac_sha1_hex_robust( encode_utf8( 'A' ), 'B' ), 'hmac_sha1_hex_robust: encode_utf8 A B => 36c54f255b575a2db58921d116b37c8af94c08cd' ) ; + is( '36c54f255b575a2db58921d116b37c8af94c08cd', hmac_sha1_hex_robust( qq{\x{0041}}, 'B' ), 'hmac_sha1_hex_robust: \x{0041} B => 36c54f255b575a2db58921d116b37c8af94c08cd' ) ; + + # http://unicode.scarfboy.com/?s=U%2B6536 + # Good + is( '4199f02773d1cd5599b1a8f2d024bdceb8b48e0b', hmac_sha1_hex_robust( 'æâ€Â¶' ), 'hmac_sha1_hex_robust: æâ€Â¶ => 4199f02773d1cd5599b1a8f2d024bdceb8b48e0b' ) ; + is( '4199f02773d1cd5599b1a8f2d024bdceb8b48e0b', hmac_sha1_hex_robust( encode_utf8(qq{\x{6536}}) ), 'hmac_sha1_hex_robust: encode_utf8 \x{6536} => 4199f02773d1cd5599b1a8f2d024bdceb8b48e0b' ) ; + # Bad + is( 'e82217119628ad03e659cc89671d05ea4cee7238', hmac_sha1_hex_robust( encode_utf8( 'æâ€Â¶' ) ), 'hmac_sha1_hex_robust: encode_utf8 æâ€Â¶ => e82217119628ad03e659cc89671d05ea4cee7238' ) ; + # Good + is( '4199f02773d1cd5599b1a8f2d024bdceb8b48e0b', hmac_sha1_hex_robust( qq{\x{6536}} ), 'hmac_sha1_hex_robust: \x{6536} => 4199f02773d1cd5599b1a8f2d024bdceb8b48e0b' ) ; + # Good again + is( '4199f02773d1cd5599b1a8f2d024bdceb8b48e0b', my $eval = eval{ hmac_sha1_hex_robust( qq{\x{6536}} ) }, 'hmac_sha1_hex_robust: \x{6536} => undef' ) ; + + note( 'Leaving tests_hmac_sha1_hex_robust()' ) ; + return ; +} + + +sub hmac_sha1_hex_robust +{ + my $string = shift ; + my $val ; + if ( defined( $val = eval{ hmac_sha1_hex( $string, @ARG ) } ) ) + { + return $val ; + } + elsif( defined( $val = eval{ hmac_sha1_hex( encode_utf8( $string ), @ARG ) } ) ) + { + return $val ; + } + else + { + return ; + } +} + +sub tests_createhashfileifneeded +{ + note( 'Entering tests_createhashfileifneeded()' ) ; + + is( undef, createhashfileifneeded( ), 'createhashfileifneeded: no parameters => undef' ) ; + + note( 'Leaving tests_createhashfileifneeded()' ) ; + return ; +} + +sub createhashfileifneeded +{ + my $hashfile = shift ; + my $hashkey = shift || rand32( ) ; + + # no name + if ( ! $hashfile ) { + return ; + } + # already there + if ( -e -r $hashfile ) { + return $hashfile ; + } + # not creatable + if ( ! -w dirname( $hashfile ) ) { + return ; + } + # creatable + open my $FILE_HANDLE, '>', $hashfile + or do { + myprint( "Could not open $hashfile for writing. Check permissions or disk space." ) ; + return ; + } ; + myprint( "Writing random hashkey in $hashfile, once for all times\n" ) ; + print $FILE_HANDLE $hashkey ; + close $FILE_HANDLE ; + # Should be there now + if ( -e -r $hashfile ) { + return $hashfile ; + } + # unknown failure + return ; +} + +sub tests_rand32 +{ + note( 'Entering tests_rand32()' ) ; + + my $string = rand32( ) ; + myprint( "$string\n" ) ; + is( 32, length( $string ), 'rand32: 32 characters long' ) ; + is( 32, length( rand32( ) ), 'rand32: 32 characters long, another one' ) ; + + note( 'Leaving tests_rand32()' ) ; + return ; +} + +sub rand32 +{ + my @chars = ( "a".."z" ) ; + my $string; + $string .= $chars[rand @chars] for 1..32 ; + return $string ; +} + +sub imap_id_stuff +{ + my $mysync = shift ; + + if ( not $mysync->{id} ) { return ; } ; + + $mysync->{h1_imap_id} = imap_id( $mysync, $mysync->{imap1}, 'Host1' ) ; + #myprint( 'Host1: ' . $mysync->{h1_imap_id} ) ; + $mysync->{h2_imap_id} = imap_id( $mysync, $mysync->{imap2}, 'Host2' ) ; + #myprint( 'Host2: ' . $mysync->{h2_imap_id} ) ; + + return ; +} + +sub imap_id +{ + my ( $mysync, $imap, $Side ) = @_ ; + + if ( not $mysync->{id} ) { return q{} ; } ; + + $Side ||= q{} ; + my $imap_id_response = q{} ; + + if ( not $imap->has_capability( 'ID' ) ) { + $imap_id_response = 'No ID capability' ; + myprint( "$Side: No ID capability\n" ) ; + }else{ + my $id_inp = imapsync_id( $mysync, { side => lc $Side } ) ; + myprint( "\n$Side: found ID capability. Sending/receiving ID, presented in raw IMAP for now.\n" + . "In order to avoid sending/receiving ID, use option --noid\n" ) ; + my $debug_before = $imap->Debug( ) ; + $imap->Debug( 1 ) ; + my $id_out = $imap->tag_and_run( 'ID ' . $id_inp ) ; + #my $id_out = $imap->tag_and_run( 'ID NIL' ) ; + myprint( "\n" ) ; + $imap->Debug( $debug_before ) ; + #$imap_id_response = Data::Dumper->Dump( [ $id_out ], [ 'IMAP_ID' ] ) ; + } + return( $imap_id_response ) ; +} + +sub imapsync_id +{ + my $mysync = shift ; + my $overhashref = shift ; + # See http://tools.ietf.org/html/rfc2971.html + + my $imapsync_id = { } ; + + my $imapsync_id_lamiral = { + name => 'imapsync', + version => imapsync_version( $mysync ), + os => $OSNAME, + vendor => 'Gilles LAMIRAL', + 'support-url' => 'https://imapsync.lamiral.info/', + # Example of date-time: 19-Sep-2015 08:56:07 + date => date_from_rcs( q{$Date: 2022/01/12 21:28:37 $ } ), + } ; + + my $imapsync_id_github = { + name => 'imapsync', + version => imapsync_version( $mysync ), + os => $OSNAME, + vendor => 'github', + 'support-url' => 'https://github.com/imapsync/imapsync', + date => date_from_rcs( q{$Date: 2022/01/12 21:28:37 $ } ), + } ; + + $imapsync_id = $imapsync_id_lamiral ; + #$imapsync_id = $imapsync_id_github ; + my %mix = ( %{ $imapsync_id }, %{ $overhashref } ) ; + my $imapsync_id_str = format_for_imap_arg( \%mix ) ; + #myprint( "$imapsync_id_str\n" ) ; + return( $imapsync_id_str ) ; +} + +sub tests_imapsync_id +{ + note( 'Entering tests_imapsync_id()' ) ; + + my $mysync ; + ok( '("name" "imapsync" "version" "111" "os" "beurk" "vendor" "Gilles LAMIRAL" "support-url" "https://imapsync.lamiral.info/" "date" "22-12-1968" "side" "host1")' + eq imapsync_id( $mysync, + { + version => 111, + os => 'beurk', + date => '22-12-1968', + side => 'host1' + } + ), + 'tests_imapsync_id override' + ) ; + + note( 'Leaving tests_imapsync_id()' ) ; + return ; +} + +sub format_for_imap_arg +{ + my $ref = shift ; + + my $string = q{} ; + my %terms = %{ $ref } ; + my @terms = ( ) ; + if ( not ( %terms ) ) { return( 'NIL' ) } ; + # sort like in RFC then add extra key/values + foreach my $key ( qw( name version os os-version vendor support-url address date command arguments environment) ) { + if ( $terms{ $key } ) { + push @terms, $key, $terms{ $key } ; + delete $terms{ $key } ; + } + } + push @terms, %terms ; + $string = '(' . ( join q{ }, map { '"' . $_ . '"' } @terms ) . ')' ; + return( $string ) ; +} + + + +sub tests_format_for_imap_arg +{ + note( 'Entering tests_format_for_imap_arg()' ) ; + + ok( 'NIL' eq format_for_imap_arg( { } ), 'format_for_imap_arg empty hash ref' ) ; + ok( '("name" "toto")' eq format_for_imap_arg( { name => 'toto' } ), 'format_for_imap_arg { name => toto }' ) ; + ok( '("name" "toto" "key" "val")' eq format_for_imap_arg( { name => 'toto', key => 'val' } ), 'format_for_imap_arg 2 x key val' ) ; + + note( 'Leaving tests_format_for_imap_arg()' ) ; + return ; +} + +sub quota +{ + my ( $mysync, $imap, $side ) = @_ ; + + my %side = ( + h1 => 'Host1', + h2 => 'Host2', + ) ; + my $Side = $side{ $side } ; + my $debug_before = $imap->Debug( ) ; + $imap->Debug( 1 ) ; + if ( not $imap->has_capability( 'QUOTA' ) ) + { + myprint( "$Side: No QUOTA capability found, skipping it.\n" ) ; + $imap->Debug( $debug_before ) ; + return ; + } ; + myprint( "\n$Side: QUOTA capability found, presented in raw IMAP on next lines\n" ) ; + my $getquotaroot = $imap->getquotaroot( 'INBOX' ) ; + # Gmail INBOX quotaroot is "" but with it Mail::IMAPClient does a literal GETQUOTA {2} \n "" + #$imap->quota( 'ROOT' ) ; + #$imap->quota( '""' ) ; + myprint( "\n" ) ; + $imap->Debug( $debug_before ) ; + my $quota_limit_bytes = quota_extract_storage_limit_in_bytes( $mysync, $getquotaroot ) ; + my $quota_current_bytes = quota_extract_storage_current_in_bytes( $mysync, $getquotaroot ) ; + $mysync->{$side}->{quota_limit_bytes} = $quota_limit_bytes ; + $mysync->{$side}->{quota_current_bytes} = $quota_current_bytes ; + my $quota_percent ; + if ( $quota_limit_bytes > 0 ) { + $quota_percent = mysprintf( '%.2f', $NUMBER_100 * $quota_current_bytes / $quota_limit_bytes ) ; + }else{ + $quota_percent = 0 ; + } + myprint( "$Side: Quota current storage is $quota_current_bytes bytes. Limit is $quota_limit_bytes bytes. So $quota_percent % full\n" ) ; + if ( $QUOTA_PERCENT_LIMIT < $quota_percent ) { + my $error = "$Side: $quota_percent % full: it is time to find a bigger place! ( $quota_current_bytes bytes / $quota_limit_bytes bytes )\n" ; + errors_incr( $mysync, $error ) ; + } + return ; +} + +sub tests_quota_extract_storage_limit_in_bytes +{ + note( 'Entering tests_quota_extract_storage_limit_in_bytes()' ) ; + + my $mysync = {} ; + my $imap_output = [ + '* QUOTAROOT "INBOX" "Storage quota" "Messages quota"', + '* QUOTA "Storage quota" (STORAGE 1 104857600)', + '* QUOTA "Messages quota" (MESSAGE 2 100000)', + '5 OK Getquotaroot completed.' + ] ; + ok( $NUMBER_104_857_600 * $KIBI == quota_extract_storage_limit_in_bytes( $mysync, $imap_output ), 'quota_extract_storage_limit_in_bytes ') ; + + note( 'Leaving tests_quota_extract_storage_limit_in_bytes()' ) ; + return ; +} + +sub quota_extract_storage_limit_in_bytes +{ + my $mysync = shift ; + my $imap_output = shift ; + + my $limit_kb ; + $limit_kb = ( map { /.*\(\s*STORAGE\s+\d+\s+(\d+)\s*\)/x ? $1 : () } @{ $imap_output } )[0] ; + $limit_kb ||= 0 ; + $mysync->{ debug } and myprint( "storage_limit_kb = $limit_kb\n" ) ; + return( $KIBI * $limit_kb ) ; +} + + +sub tests_quota_extract_storage_current_in_bytes +{ + note( 'Entering tests_quota_extract_storage_current_in_bytes()' ) ; + + my $mysync = {} ; + my $imap_output = [ + '* QUOTAROOT "INBOX" "Storage quota" "Messages quota"', + '* QUOTA "Storage quota" (STORAGE 1 104857600)', + '* QUOTA "Messages quota" (MESSAGE 2 100000)', + '5 OK Getquotaroot completed.' + ] ; + ok( 1*$KIBI == quota_extract_storage_current_in_bytes( $mysync, $imap_output ), 'quota_extract_storage_current_in_bytes: 1 => 1024 ') ; + + note( 'Leaving tests_quota_extract_storage_current_in_bytes()' ) ; + return ; +} + +sub quota_extract_storage_current_in_bytes +{ + my $mysync = shift ; + my $imap_output = shift ; + + my $current_kb ; + $current_kb = ( map { /.*\(\s*STORAGE\s+(\d+)\s+\d+\s*\)/x ? $1 : () } @{ $imap_output } )[0] ; + $current_kb ||= 0 ; + $mysync->{ debug } and myprint( "storage_current_kb = $current_kb\n" ) ; + return( $KIBI * $current_kb ) ; + +} + + +sub automap +{ + my ( $mysync ) = @_ ; + + if ( $mysync->{automap} ) { + myprint( "Turned on automapping folders ( use --noautomap to turn off automapping )\n" ) ; + }else{ + myprint( "Turned off automapping folders ( use --automap to turn on automapping )\n" ) ; + return ; + } + + $mysync->{h1_special} = special_from_folders_hash( $mysync, $mysync->{imap1}, 'Host1' ) ; + $mysync->{h2_special} = special_from_folders_hash( $mysync, $mysync->{imap2}, 'Host2' ) ; + + build_possible_special( $mysync ) ; + build_guess_special( $mysync ) ; + build_automap( $mysync ) ; + + return ; +} + + + + +sub build_guess_special +{ + my ( $mysync ) = shift ; + + foreach my $h1_fold ( sort keys %{ $mysync->{h1_folders_all} } ) { + my $special = guess_special( $h1_fold, $mysync->{possible_special}, $mysync->{h1_prefix} ) ; + if ( $special ) { + $mysync->{h1_special_guessed}{$h1_fold} = $special ; + my $already_guessed = $mysync->{h1_special_guessed}{$special} ; + if ( $already_guessed ) { + myprint( "Host1: $h1_fold not $special because set to $already_guessed\n" ) ; + }else{ + $mysync->{h1_special_guessed}{$special} = $h1_fold ; + } + } + } + foreach my $h2_fold ( sort keys %{ $mysync->{h2_folders_all} } ) { + my $special = guess_special( $h2_fold, $mysync->{possible_special}, $mysync->{h2_prefix} ) ; + if ( $special ) { + $mysync->{h2_special_guessed}{$h2_fold} = $special ; + my $already_guessed = $mysync->{h2_special_guessed}{$special} ; + if ( $already_guessed ) { + myprint( "Host2: $h2_fold not $special because set to $already_guessed\n" ) ; + }else{ + $mysync->{h2_special_guessed}{$special} = $h2_fold ; + } + } + } + return ; +} + +sub guess_special +{ + my( $folder, $possible_special_ref, $prefix ) = @_ ; + + my $folder_no_prefix = $folder ; + $folder_no_prefix =~ s/\Q${prefix}\E//xms ; + #$debug and myprint( "folder_no_prefix: $folder_no_prefix\n" ) ; + + my $guess_special = $possible_special_ref->{ $folder } + || $possible_special_ref->{ $folder_no_prefix } + || q{} ; + + return( $guess_special ) ; +} + +sub tests_guess_special +{ + note( 'Entering tests_guess_special()' ) ; + + my $possible_special_ref = build_possible_special( my $mysync ) ; + ok( '\Sent' eq guess_special( 'Sent', $possible_special_ref, q{} ) ,'guess_special: Sent => \Sent' ) ; + ok( q{} eq guess_special( 'Blabla', $possible_special_ref, q{} ) ,'guess_special: Blabla => q{}' ) ; + ok( '\Sent' eq guess_special( 'INBOX.Sent', $possible_special_ref, 'INBOX.' ) ,'guess_special: INBOX.Sent => \Sent' ) ; + ok( '\Sent' eq guess_special( 'IN BOX.Sent', $possible_special_ref, 'IN BOX.' ) ,'guess_special: IN BOX.Sent => \Sent' ) ; + + note( 'Leaving tests_guess_special()' ) ; + return ; +} + +sub build_automap +{ + my $mysync = shift ; + $mysync->{ debug } and myprint( "Entering build_automap\n" ) ; + foreach my $h1_fold ( @{ $mysync->{h1_folders_wanted} } ) { + my $h2_fold ; + my $h1_special = $mysync->{h1_special}{$h1_fold} ; + my $h1_special_guessed = $mysync->{h1_special_guessed}{$h1_fold} ; + + # Case 1: special on both sides. + if ( $h1_special + and exists $mysync->{h2_special}{$h1_special} ) { + $h2_fold = $mysync->{h2_special}{$h1_special} ; + $mysync->{f1f2auto}{ $h1_fold } = $h2_fold ; + next ; + } + # Case 2: special on host1, not on host2 + if ( $h1_special + and ( not exists $mysync->{h2_special}{$h1_special} ) + and ( exists $mysync->{h2_special_guessed}{$h1_special} ) + ) { + # special_guessed on host2 + $h2_fold = $mysync->{h2_special_guessed}{$h1_special} ; + $mysync->{f1f2auto}{ $h1_fold } = $h2_fold ; + next ; + } + # Case 3: no special on host1, special on host2 + if ( ( not $h1_special ) + and ( $h1_special_guessed ) + and ( exists $mysync->{h2_special}{$h1_special_guessed} ) + ) { + $h2_fold = $mysync->{h2_special}{$h1_special_guessed} ; + $mysync->{f1f2auto}{ $h1_fold } = $h2_fold ; + next ; + } + # Case 4: no special on both sides. + if ( ( not $h1_special ) + and ( $h1_special_guessed ) + and ( not exists $mysync->{h2_special}{$h1_special_guessed} ) + and ( exists $mysync->{h2_special_guessed}{$h1_special_guessed} ) + ) { + $h2_fold = $mysync->{h2_special_guessed}{$h1_special_guessed} ; + $mysync->{f1f2auto}{ $h1_fold } = $h2_fold ; + next ; + } + } + return( $mysync->{f1f2auto} ) ; +} + +# I will not add what there is at: +# http://stackoverflow.com/questions/2185391/localized-gmail-imap-folders/2185548#2185548 +# because it works well without +sub build_possible_special +{ + my $mysync = shift ; + my $possible_special = { } ; + # All|Archive|Drafts|Flagged|Junk|Sent|Trash + + $possible_special->{'\All'} = [ 'All', 'All Messages', '&BBIEQQQ1-' ] ; + $possible_special->{'\Archive'} = [ 'Archive', 'Archives', '&BBAEQARFBDgEMg-' ] ; + $possible_special->{'\Drafts'} = [ 'Drafts', 'DRAFTS', '&BCcENQRABD0EPgQyBDgEOgQ4-', 'Szkice', 'Wersje robocze' ] ; + $possible_special->{'\Flagged'} = [ 'Flagged', 'Starred', '&BB8EPgQ8BDUERwQ1BD0EPQRLBDU-' ] ; + $possible_special->{'\Junk'} = [ 'Junk', 'junk', 'Spam', 'SPAM', '&BCEEPwQwBDw-', + 'Potwierdzony spam', 'Wiadomo&AVs-ci-&AVs-mieci', + 'Junk E-Mail', 'Junk Email'] ; + $possible_special->{'\Sent'} = [ 'Sent', 'Sent Messages', 'Sent Items', + 'Gesendete Elemente', 'Gesendete Objekte', + '&AMk-l&AOk-ments envoy&AOk-s', 'E&AwE-le&AwE-ments envoye&AwE-s', 'Envoy&AOk-', 'Objets envoy&AOk-s', + 'Elementos enviados', + '&kAFP4W4IMH8wojCkMMYw4A-', + '&BB4EQgQ,BEAEMAQyBDsENQQ9BD0ESwQ1-', + 'Elementy wys&AUI-ane'] ; + $possible_special->{'\Trash'} = [ 'Trash', 'TRASH', + '&BCMENAQwBDsENQQ9BD0ESwQ1-', '&BBoEPgRABDcEOAQ9BDA-', + 'Kosz', + 'Deleted Items', 'Deleted Messages' ] ; + + + foreach my $special ( qw( \All \Archive \Drafts \Flagged \Junk \Sent \Trash ) ){ + foreach my $possible_folder ( @{ $possible_special->{$special} } ) { + $possible_special->{ $possible_folder } = $special ; + } ; + } + $mysync->{possible_special} = $possible_special ; + $mysync->{ debug } and myprint( Data::Dumper->Dump( [ $possible_special ], [ 'possible_special' ] ) ) ; + return( $possible_special ) ; +} + +sub tests_special_from_folders_hash +{ + note( 'Entering tests_special_from_folders_hash()' ) ; + + my $mysync = {} ; + require_ok( "Test::MockObject" ) ; + my $imapT = Test::MockObject->new( ) ; + + is( undef, special_from_folders_hash( ), 'special_from_folders_hash: no args' ) ; + is( undef, special_from_folders_hash( $mysync ), 'special_from_folders_hash: undef args' ) ; + is_deeply( {}, special_from_folders_hash( $mysync, $imapT ), 'special_from_folders_hash: $imap void' ) ; + + $imapT->mock( 'folders_hash', sub { return( [ { name => 'Sent', attrs => [ '\Sent' ] } ] ) } ) ; + + is_deeply( { Sent => '\Sent', '\Sent' => 'Sent' }, + special_from_folders_hash( $mysync, $imapT ), 'special_from_folders_hash: $imap \Sent' ) ; + + note( 'Leaving tests_special_from_folders_hash()' ) ; + return( ) ; +} + +sub special_from_folders_hash +{ + my ( $mysync, $imap, $side ) = @_ ; + my %special = ( ) ; + + if ( ! defined $imap ) { return ; } + $side = defined $side ? $side : 'Host?' ; + + if ( ! $imap->can( 'folders_hash' ) ) { + my $error = "$side: To have automagic rfc6154 folder mapping, upgrade Mail::IMAPClient >= 3.34\n" ; + errors_incr( $mysync, $error ) ; + return( \%special ) ; # empty hash ref + } + my $folders_hash = $imap->folders_hash( ) ; + foreach my $fhash (@{ $folders_hash } ) { + my @special = grep { /\\(?:All|Archive|Drafts|Flagged|Junk|Sent|Trash)/x } @{ $fhash->{attrs} } ; + if ( @special ) { + my $special = $special[0] ; # keep first one. Could be not very good. + if ( exists $special{ $special } ) { + myprintf( "%s: special %-20s = %s already assigned to %s\n", + $side, $fhash->{name}, join( q{ }, @special ), $special{ $special } ) ; + }else{ + myprintf( "%s: special %-20s = %s\n", + $side, $fhash->{name}, join( q{ }, @special ) ) ; + $special{ $special } = $fhash->{name} ; + $special{ $fhash->{name} } = $special ; # double entry value => key + } + } + } + myprint( "\n" ) if ( %special ) ; + return( \%special ) ; +} + + +sub tests_errors_log +{ + note( 'Entering tests_errors_log()' ) ; + is( undef, errors_log( ), 'errors_log: no args => undef' ) ; + my $mysync = {} ; + is( undef, errors_log( $mysync ), 'errors_log: empty => undef' ) ; + is_deeply( [ 'aieaie' ], [ errors_log( $mysync, 'aieaie' ) ], 'errors_log: aieaie => aieaie' ) ; + # cumulative + is_deeply( [ 'aieaie' ], [ errors_log( $mysync ) ], 'errors_log: nothing more => aieaie' ) ; + is_deeply( [ 'aieaie', 'ouille' ], [ errors_log( $mysync, 'ouille' ) ], 'errors_log: ouille => aieaie ouille' ) ; + is_deeply( [ 'aieaie', 'ouille' ], [ errors_log( $mysync ) ], 'errors_log: nothing more => aieaie ouille' ) ; + note( 'Leaving tests_errors_log()' ) ; + return ; +} + +sub errors_log +{ + my ( $mysync, @error ) = @ARG ; + + if ( ! $mysync->{errors_log} ) { + $mysync->{errors_log} = [] ; + } + + if ( @error ) { + push @{ $mysync->{errors_log} }, join( q{}, @error ) ; + } + if ( @{ $mysync->{errors_log} } ) { + return @{ $mysync->{errors_log} } ; + } + else { + return ; + } +} + + + +sub tests_comment_of_error_type +{ + note( 'Entering tests_comment_of_error_type()' ) ; + + is( undef, comment_of_error_type( ), 'comment_of_error_type: no args => undef' ) ; + + my $mysync = { } ; + is( undef, comment_of_error_type( $mysync ), 'comment_of_error_type: undef => undef' ) ; + + is( "", comment_of_error_type( $mysync, '' ), 'comment_of_error_type: "" => ""' ) ; + is( "", comment_of_error_type( $mysync, 'blabla' ), 'comment_of_error_type: blabla => ""' ) ; + + is( "", comment_of_error_type( $mysync, 'ERR_UNCLASSIFIED' ), 'comment_of_error_type: ERR_UNCLASSIFIED => ""' ) ; + + like( comment_of_error_type( $mysync, 'ERR_OVERQUOTA' ), qr{100% full}, 'comment_of_error_type: ERR_OVERQUOTA => matches 100% full' ) ; + + + + note( 'Leaving tests_comment_of_error_type()' ) ; + return ; +} + +sub comment_of_error_type +{ + my $mysync = shift @ARG ; + my $error_type = shift @ARG ; + + if ( ! defined $mysync ) { return ; } + if ( ! defined $error_type ) { return ; } + + my $comment ; + + if ( exists( $COMMENT_OF_ERR_TYPE{ $error_type } ) ) + { + $comment = $COMMENT_OF_ERR_TYPE{ $error_type }->( $mysync ) ; + } + else + { + $comment = "" ; + } + return $comment ; +} + + + +sub tests_error_type +{ + note( 'Entering tests_error_type()' ) ; + + is( 'ERR_NOTHING_REPORTED', error_type( ), 'error_type: no args => ERR_NOTHING_REPORTED' ) ; + is( 'ERR_NOTHING_REPORTED', error_type( '' ), 'error_type: empty string => ERR_NOTHING_REPORTED' ) ; + + is( 'ERR_UNCLASSIFIED', error_type( 'ERR_UNCLASSIFIED' ), 'error_type: ERR_UNCLASSIFIED => ERR_UNCLASSIFIED' ) ; + is( 'ERR_UNCLASSIFIED', error_type( 'aie' ), 'error_type: aie => ERR_UNCLASSIFIED' ) ; + is( 'ERR_UNCLASSIFIED', error_type( 'ouille' ), 'error_type: ouille => ERR_UNCLASSIFIED' ) ; + + is( 'ERR_Host1_FETCH', error_type( 'Message xxx could not be fetched: blabla' ), + 'error_type: could not be fetched => ERR_Host1_FETCH' + ) ; + + is( 'ERR_APPEND_SIZE', + error_type( 'could not append message xxx: BAD maximum message size exceeded' ), + 'error_type: could not append message xxx: BAD maximum message size exceeded => ERR_APPEND_SIZE' + ) ; + + is( 'ERR_OVERQUOTA', + error_type( 'Quota limit will be exceeded' ), + 'error_type: Quota limit will be exceeded => ERR_OVERQUOTA' + ) ; + + is( 'ERR_APPEND', error_type( 'could not append' ), 'error_type: could not append => ERR_APPEND' ) ; + + is( 'ERR_CREATE', + error_type( 'Could not create folder' ), + 'error_type: Could not create folder => ERR_CREATE' + ) ; + + is( 'ERR_SELECT', + error_type( 'Could not select: blabla' ), + 'error_type: Could not select: blabla => ERR_SELECT' + ) ; + + + # + #Maximum bytes transferred reached, 423 >= 100, ending sync + is( 'ERR_TRANSFER_EXCEEDED', + error_type( 'Maximum bytes transferred reached, blabla' ), + 'error_type: Maximum bytes transferred reached, blabla => ERR_TRANSFER_EXCEEDED' + ) ; + + # + is( 'ERR_CONNECTION_FAILURE_HOST1', + error_type( 'Host1 failure: can not open imap connection on host1 [badhostkaka] with user [tata]: Unable to connect to badhostkaka: Invalid argument' ), + 'error_type: can not open imap connection on host1 => ERR_CONNECTION_FAILURE_HOST1' + ) ; + + is( 'ERR_CONNECTION_FAILURE_HOST2', + error_type( 'Host2 failure: can not open imap connection on host2 [badhostkiki] with user [titi]: Unable to connect to badhostkiki: Invalid argument' ), + 'error_type: can not open imap connection on host2 => ERR_CONNECTION_FAILURE_HOST2' + ) ; + + is( 'ERR_APPEND_VIRUS', + error_type( 'could not append ( Subject:[For Your Consideration], Date:["29-Nov-2016 03:21:10 -0800"], Size:[5505], Flags:[\Seen] ) to folder INBOX: 275 NO Message refused because it contains a virus' ), + 'error_type: could not append ... virus => ERR_APPEND_VIRUS' + ) ; + + + is( 'ERR_FLAGS', + error_type( 'Host2: flags msg INBOX/957910 could not add flags [PasGlop \PasGlopRe]: 33 NO Error in IMAP command received by server.' ), + 'error_type: could not add flags => ERR_FLAGS' + ) ; + + + note( 'Leaving tests_error_type()' ) ; + return ; +} + + + +# Could be implemented with https://metacpan.org/pod/Tie::RegexpHash +# with just a hash of error regexes as keys and types as values. + +sub error_type +{ + my $error = shift ; + + if ( ! defined $error ) { return 'ERR_NOTHING_REPORTED' ; } + if ( ! $error ) { return 'ERR_NOTHING_REPORTED' ; } + + # + if ( $error =~ m{Host1 failure: Error login on} ) { return 'ERR_AUTHENTICATION_FAILURE_USER1' } ; + if ( $error =~ m{Host2 failure: Error login on} ) { return 'ERR_AUTHENTICATION_FAILURE_USER2' } ; + + if ( $error =~ m{Host. failure: Can not go to tls encryption on host.} ) { return 'ERR_EXIT_TLS_FAILURE' } ; + # + + if ( $error =~ m{could not be fetched:} ) { return 'ERR_Host1_FETCH' } ; + + # could not append .*BAD maximum message size exceeded + # could not append.*Maximum size of appendable message has been exceeded + if ( $error =~ m{could not append .*BAD maximum message size exceeded} ) + { return 'ERR_APPEND_SIZE' ; } ; + + if ( $error =~ m{could not append.*Maximum size of appendable message has been exceeded} ) + { return 'ERR_APPEND_SIZE' ; } ; + + # Could not create folder *[OVERQUOTA] Not enough disk quota + # could not append .*[OVERQUOTA] Not enough disk quota + # could not append .*[OVERQUOTA] Mailbox is full / Blocks limit exceeded / Inode limit exceeded + if ( $error =~ m{OVERQUOTA} ) { return 'ERR_OVERQUOTA' ; } ; + if ( $error =~ m{Quota limit will be exceeded} ) { return 'ERR_OVERQUOTA' ; } ; + if ( $error =~ m{full: it is time to find a bigger place} ) { return 'ERR_OVERQUOTA' ; } ; + + # could not append ... to folder INBOX: 276 NO Message refused because it contains a virus + if ( $error =~ m{could not append.*virus} ) + { return 'ERR_APPEND_VIRUS' ; } ; + + # could not append .*Write failed 'Broken pipe' + # could not append .*timeout waiting .* for data from server + # could not append .*BAD Invalid Arguments: Unable to parse message + # could not append .*BAD Command Argument Error. 11 + # could not append .*NO header limit reached + if ( $error =~ m{could not append} ) { return 'ERR_APPEND' ; } ; + + # could not add flags + if ( $error =~ m{could not add flags} ) { return 'ERR_FLAGS' ; } ; + + + # Could not create folder .*Invalid mailbox name + if ( $error =~ m{Could not create folder} ) { return 'ERR_CREATE' ; } ; + + + # Could not select:.*NO [NOPERM] Permission denied + # Could not select:.*NO Mailbox doesn't exist + # Could not select:.*NO [SERVERBUG] Internal error occurred. + # Could not select:.*[CANNOT] Mailbox isn't a valid mbox file + if ( $error =~ m{Could not select:} ) { return 'ERR_SELECT' ; } ; + + #Maximum bytes transferred reached, 423 >= 100, ending sync + if ( $error =~ m{Maximum bytes transferred reached} ) { return 'ERR_TRANSFER_EXCEEDED' ; } ; + + if ( $error =~ m{can not open imap connection on host1} ) { return 'ERR_CONNECTION_FAILURE_HOST1' ; } ; + if ( $error =~ m{can not open imap connection on host2} ) { return 'ERR_CONNECTION_FAILURE_HOST2' ; } ; + + # Default is ERR_UNCLASSIFIED + return 'ERR_UNCLASSIFIED' ; + +} + +sub tests_errorclassify +{ + note( 'Entering tests_errorclassify()' ) ; + + is( undef, errorclassify( ), 'errorclassify: no args => undef' ) ; + + is_deeply( { 'ERR_UNCLASSIFIED' => 1 }, errorclassify( 'aie' ), 'errorclassify: aie => { ERR_UNCLASSIFIED => 1 }' ) ; + is_deeply( { 'ERR_UNCLASSIFIED' => 2 }, errorclassify( 'aie', 'ouille' ), 'errorclassify: aie ouille => { ERR_UNCLASSIFIED => 2 }' ) ; + is_deeply( { 'ERR_UNCLASSIFIED' => 2, 'ERR_NOTHING_REPORTED' => 1 }, errorclassify( 'aie', 'ouille', '' ), 'errorclassify: aie ouille "" => { ERR_UNCLASSIFIED => 2 }' ) ; + is_deeply( { 'ERR_UNCLASSIFIED' => 3 }, errorclassify( 'aie', 'ouille', 'aie' ), 'errorclassify: aie ouille aie => { ERR_UNCLASSIFIED => 3 }' ) ; + is_deeply( { 'ERR_UNCLASSIFIED' => 1, 'ERR_OVERQUOTA' => 2 }, errorclassify( 'aie', 'OVERQUOTA pipi', 'OVERQUOTA caca' ), 'errorclassify: aie OVERQUOTA OVERQUOTA' ) ; + is_deeply( { 'ERR_NOTHING_REPORTED' => 1 }, errorclassify( '' ), 'errorclassify: "" => { ERR_NOTHING_REPORTED => 1 }' ) ; + is_deeply( { 'ERR_NOTHING_REPORTED' => 2 }, errorclassify( '', '' ), 'errorclassify: "", "" => { ERR_NOTHING_REPORTED => 1 }' ) ; + + note( 'Leaving tests_errorclassify()' ) ; + return ; +} + + + +sub errorclassify +{ + my @errors = @ARG ; + + if ( ! @errors ) { return ; } ; + + my $error_type_count = { } ; + foreach my $error ( @errors ) + { + my $error_type = error_type( $error ) ; + $error_type_count->{ $error_type }++ ; + } + + return $error_type_count ; +} + +sub tests_most_common_error +{ + note( 'Entering tests_most_common_error()' ) ; + + is( 'ERR_NOTHING_REPORTED', most_common_error( ), 'most_common_error: no args => ERR_NOTHING_REPORTED' ) ; + is( 'ERR_NOTHING_REPORTED', most_common_error( {} ), 'most_common_error: empty hash ref => ERR_NOTHING_REPORTED' ) ; + is( 'ERR_NOTHING_REPORTED', most_common_error( 'blabla' ), 'most_common_error: not a hash ref => ERR_NOTHING_REPORTED' ) ; + + is( 'ERR_FOO', most_common_error( { ERR_FOO => 1 } ), 'most_common_error: { ERR_FOO => 1 } => ERR_FOO' ) ; + is( 'ERR_BAR', most_common_error( { ERR_FOO => 1, ERR_BAR => 2 } ), 'most_common_error: { ERR_FOO => 1, ERR_BAR => 2 } => ERR_BAR' ) ; + is( 'ERR_FOO', most_common_error( { ERR_FOO => 2, ERR_BAR => 1 } ), 'most_common_error: { ERR_FOO => 2, ERR_BAR => 1 } => ERR_FOO' ) ; + # exaequo => first lexical wins. ERR_BAR <= ERR_FOO + is( 'ERR_BAR', most_common_error( { ERR_FOO => 2, ERR_BAR => 2 } ), 'most_common_error: { ERR_FOO => 2, ERR_BAR => 2 } => ERR_BAR' ) ; + + is( 'A', most_common_error( { A => 5, B => 5, C => 5 } ), 'most_common_error: { A => 5, B => 5, C => 5 } => A' ) ; + is( 'B', most_common_error( { A => 5, B => 6, C => 6 } ), 'most_common_error: { A => 5, B => 6, C => 6 } => B' ) ; + is( 'C', most_common_error( { A => 5, B => 5, C => 7 } ), 'most_common_error: { A => 5, B => 5, C => 7 } => C' ) ; + is( 'C', most_common_error( { A => 5, B => 6, C => 7 } ), 'most_common_error: { A => 5, B => 5, C => 7 } => C' ) ; + + note( 'Leaving tests_most_common_error()' ) ; + return ; +} + + + +sub most_common_error +{ + my $errors_counted_ref = shift ; + + if ( ! defined $errors_counted_ref ) { return 'ERR_NOTHING_REPORTED' ; } + + if ( 'HASH' ne ref $errors_counted_ref ) { return 'ERR_NOTHING_REPORTED' ; } + + # empty hash + if ( !%{ $errors_counted_ref } ) { return 'ERR_NOTHING_REPORTED' ; } + + # non empty hash + # in case of equality the winner error is the first in alphabetic order + my $most_common_error = ( sort + { + $errors_counted_ref->{$b} <=> $errors_counted_ref->{$a} + || $a cmp $b + } keys %{$errors_counted_ref} )[0] ; + + return $most_common_error ; + +} + + + +sub tests_errorsanalyse +{ + note( 'Entering tests_errorsanalyse()' ) ; + + is( 'ERR_NOTHING_REPORTED', errorsanalyse( ), 'errorsanalyse: no args => ERR_NOTHING_REPORTED' ) ; + is( 'ERR_NOTHING_REPORTED', errorsanalyse( ( ) ), 'errorsanalyse: empty list => ERR_NOTHING_REPORTED' ) ; + is( 'ERR_UNCLASSIFIED', errorsanalyse( 'aie' ), 'errorsanalyse: aie => ERR_UNCLASSIFIED' ) ; + + # in case of equality, empty wins + is( 'ERR_NOTHING_REPORTED', errorsanalyse( 'aie', '' ), 'errorsanalyse: aie => ERR_UNCLASSIFIED' ) ; + is( 'ERR_NOTHING_REPORTED', errorsanalyse( '', 'aie' ), 'errorsanalyse: aie => ERR_UNCLASSIFIED' ) ; + + + is( 'ERR_UNCLASSIFIED', errorsanalyse( 'aie', 'ouille' ), 'errorsanalyse: aie, ouille => ERR_UNCLASSIFIED' ) ; + is( 'ERR_UNCLASSIFIED', errorsanalyse( 'aie', 'ouille', '' ), 'errorsanalyse: aie, ouille, "" => ERR_UNCLASSIFIED' ) ; + is( 'ERR_UNCLASSIFIED', errorsanalyse( '', 'aie', 'ouille' ), 'errorsanalyse: aie, ouille, "" => ERR_UNCLASSIFIED' ) ; + + is( 'ERR_NOTHING_REPORTED', errorsanalyse( '' ), 'errorsanalyse: "" => ERR_NOTHING_REPORTED' ) ; + is( 'ERR_NOTHING_REPORTED', errorsanalyse( ( '' ) ), 'errorsanalyse: ( "" ) => ERR_NOTHING_REPORTED' ) ; + is( 'ERR_NOTHING_REPORTED', errorsanalyse( ( '', '' ) ), 'errorsanalyse: ( "", "" ) => ERR_NOTHING_REPORTED' ) ; + + note( 'Leaving tests_errorsanalyse()' ) ; + return ; +} + + + +sub errorsanalyse +{ + my @errors = @ARG ; + my $errors_types_counted = errorclassify( @errors ) ; + + my $most_common_error = most_common_error( $errors_types_counted ) ; + + return $most_common_error ; +} + + + +sub tests_errorsdump +{ + note( 'Entering tests_errorsdump()' ) ; + + is( undef, errorsdump( ), 'errorsdump: no args => undef' ) ; + is( undef, errorsdump( ( ) ), 'errorsdump: empty list => undef' ) ; + is( "Err 1/1: ", errorsdump( '' ), 'errorsdump: one empty string => "Err 1/1: "' ) ; + is( "Err 1/1: aieaieaie", errorsdump( 'aieaieaie' ), 'errorsdump: aieaieaie => "Err 1/1: aieaieaie"' ) ; + is( "Err 1/2: Aie Err 2/2: Ouille", errorsdump( 'Aie ', 'Ouille' ), 'errorsdump: Aie Ouille => "Err 1/2: Aie Err 2/2: Ouille"' ) ; + note( 'Leaving tests_errorsdump()' ) ; + return ; +} + + +sub errorsdump +{ + if ( ! @ARG ) { return ; } + + my @errors_log = @ARG ; + my $nb_errors = @errors_log ; + my $error_num = 0 ; + my $errors_list = q{} ; + if ( @errors_log ) { + foreach my $error ( @errors_log ) + { + $error_num++ ; + $errors_list .= "Err $error_num/$nb_errors: $error" ; + } + } + return( $errors_list ) ; +} + + + +sub errors_listing +{ + my $mysync = shift ; + $mysync->{ most_common_error } = errorsanalyse( errors_log( $mysync ) ) ; + + my $errors_listing = '' ; + + if ( $mysync->{ errorsdump } ) + { + $errors_listing = join( '', + "++++ Listing $mysync->{nb_errors} errors encountered during the sync ( avoid this listing with --noerrorsdump ).\n", + errorsdump( errors_log( $mysync ) ), + ) ; + } + + $errors_listing .= join( '', + "The most frequent error is $mysync->{ most_common_error }. ", + comment_of_error_type( $mysync, $mysync->{ most_common_error } ), + "\n", + ) ; + + return $errors_listing ; +} + + +sub errors_incr +{ + my ( $mysync, @error ) = @ARG ; + $mysync->{ nb_errors }++ ; + + if ( @error ) { + errors_log( $mysync, @error ) ; + myprint( @error ) ; + } + + $mysync->{ errorsmax } ||= $ERRORS_MAX ; + + + if ( $mysync->{ nb_errors } >= $mysync->{ errorsmax } ) + { + myprint( errorsmax_msg( $mysync ) ) ; + myprint( errors_listing( $mysync ) ) ; + + if ( $mysync->{ errorsdump } ) + { + # again since errorsdump( ) can be very verbose and masquerade previous warning + myprint( errorsmax_msg( $mysync ) ) ; + } + my $exit_value = exit_value( $mysync, $mysync->{ most_common_error } ) ; + exit_clean( $mysync, $exit_value ) ; + } + return ; +} + + + +sub errorsmax_msg +{ + my $mysync = shift @ARG ; + my $msg = "Maximum number of errors $mysync->{errorsmax} reached " + . "( you can change $mysync->{errorsmax} to any value, for example 100 with --errorsmax 100 ). " + . "Exiting.\n" ; + return $msg ; +} + + + + +sub tests_live_result +{ + note( 'Entering tests_live_result()' ) ; + + my $nb_errors = shift ; + if ( $nb_errors ) { + myprint( "Live tests failed with $nb_errors errors\n" ) ; + } else { + myprint( "Live tests ended successfully\n" ) ; + } + note( 'Leaving tests_live_result()' ) ; + return ; +} + + +sub size_filtered_flag +{ + my $mysync = shift ; + my $h1_size = shift ; + + if ( defined $mysync->{ maxsize } and $h1_size >= $mysync->{ maxsize } ) { + return( 1 ) ; + } + if ( defined $minsize and $h1_size <= $minsize ) { + return( 1 ) ; + } + return( 0 ) ; +} + +sub sync_flags_fir +{ + my ( $mysync, $h1_fold, $h1_msg, $h2_fold, $h2_msg, $permanentflags2, $h1_fir_ref, $h2_fir_ref ) = @_ ; + + if ( not defined $h1_msg ) { return } ; + if ( not defined $h2_msg ) { return } ; + + my $h1_size = $h1_fir_ref->{$h1_msg}->{'RFC822.SIZE'} ; + return if size_filtered_flag( $mysync, $h1_size ) ; + + # used cached flag values for efficiency + my $h1_flags = $h1_fir_ref->{ $h1_msg }->{ 'FLAGS' } || q{} ; + my $h2_flags = $h2_fir_ref->{ $h2_msg }->{ 'FLAGS' } || q{} ; + + sync_flags( $mysync, $h1_fold, $h1_msg, $h1_flags, $h2_fold, $h2_msg, $h2_flags, $permanentflags2 ) ; + + return ; +} + +sub sync_flags_after_copy +{ + # Activated with option --syncflagsaftercopy + my( $mysync, $h1_fold, $h1_msg, $h1_flags, $h2_fold, $h2_msg, $permanentflags2 ) = @_ ; + + if ( my @h2_flags = $mysync->{imap2}->flags( $h2_msg ) ) { + my $h2_flags = "@h2_flags" ; + ( $mysync->{ debug } or $debugflags ) and myprint( "Host2: msg $h2_fold/$h2_msg flags before sync flags after copy ( $h2_flags )\n" ) ; + sync_flags( $mysync, $h1_fold, $h1_msg, $h1_flags, $h2_fold, $h2_msg, $h2_flags, $permanentflags2 ) ; + }else{ + myprint( "Host2: msg $h2_fold/$h2_msg could not get its flags for sync flags after copy\n" ) ; + } + return ; +} + +# Globals +# $debug +# $debugflags +# $permanentflags2 + + +sub sync_flags +{ + my( $mysync, $h1_fold, $h1_msg, $h1_flags, $h2_fold, $h2_msg, $h2_flags, $permanentflags2 ) = @_ ; + + ( $mysync->{ debug } or $debugflags ) and + myprint( "Host1: flags init msg $h1_fold/$h1_msg flags( $h1_flags ) Host2 msg $h2_fold/$h2_msg flags( $h2_flags )\n" ) ; + + $h1_flags = flags_for_host2( $mysync, $h1_flags, $permanentflags2 ) ; + + $h2_flags = flagscase( $h2_flags ) ; + + ( $mysync->{ debug } or $debugflags ) and + myprint( "Host1: flags filt msg $h1_fold/$h1_msg flags( $h1_flags ) Host2 msg $h2_fold/$h2_msg flags( $h2_flags )\n" ) ; + + + # compare flags - set flags if there a difference + my @h1_flags = sort split(q{ }, $h1_flags ); + my @h2_flags = sort split(q{ }, $h2_flags ); + my $diff = compare_lists( \@h1_flags, \@h2_flags ); + + $diff and ( $mysync->{ debug } or $debugflags ) + and myprint( "Host2: flags msg $h2_fold/$h2_msg replacing h2 flags( $h2_flags ) with h1 flags( $h1_flags )\n" ) ; + + # This sets flags exactly. So flags can be removed with this. + # When you remove a \Seen flag on host1 you want it + # to be removed on host2. Just add flags is not what + # we need most of the time, so no + like in "+FLAGS.SILENT". + + if ( not $mysync->{dry} and $diff and not $mysync->{imap2}->store( $h2_msg, "FLAGS.SILENT (@h1_flags)" ) ) { + my $error_msg = join q{}, "Host2: flags msg $h2_fold/$h2_msg could not add flags [@h1_flags]: ", + $mysync->{imap2}->LastError || q{}, "\n" ; + errors_incr( $mysync, $error_msg ) ; + } + + return ; +} + + + +sub _filter +{ + my $mysync = shift ; + my $str = shift or return q{} ; + my $sz = $SIZE_MAX_STR ; + my $len = length $str ; + if ( not $mysync->{ debug } and $len > $sz*2 ) { + my $beg = substr $str, 0, $sz ; + my $end = substr $str, -$sz, $sz ; + $str = $beg . '...' . $end ; + } + $str =~ s/\012?\015$//x ; + return "(len=$len) " . $str ; +} + + + +sub lost_connection +{ + my( $mysync, $imap, $error_message ) = @_; + if ( $imap->IsUnconnected( ) ) { + $mysync->{nb_errors}++ ; + my $lcomm = $imap->LastIMAPCommand || q{} ; + + my $einfo = imap_last_error( $imap ) ; + + # if string is long try reduce to a more reasonable size + $lcomm = _filter( $mysync, $lcomm ) ; + $einfo = _filter( $mysync, $einfo ) ; + myprint( "Failure: last command: $lcomm\n") if ( $mysync->{ debug } && $lcomm) ; + myprint( "Failure: lost connection $error_message: ", $einfo, "\n") ; + return( 1 ) ; + } + else{ + return( 0 ) ; + } +} + +sub imap_last_error +{ + my $imap = shift ; + my $einfo = $imap->LastError || @{$imap->History}[$LAST] || q{} ; + chomp( $einfo ) ; + return( $einfo ) ; +} + +sub tests_max +{ + note( 'Entering tests_max()' ) ; + + is( 0, max( 0 ), 'max 0 => 0' ) ; + is( 1, max( 1 ), 'max 1 => 1' ) ; + is( $MINUS_ONE, max( $MINUS_ONE ), 'max -1 => -1') ; + is( undef, max( ), 'max no arg => undef' ) ; + is( undef, max( undef ), 'undef => undef' ) ; + is( undef, max( undef, undef ), 'undef, undef => undef' ) ; + + is( $NUMBER_100, max( 1, $NUMBER_100 ), 'max 1 100 => 100' ) ; + is( $NUMBER_100, max( $NUMBER_100, 1 ), 'max 100 1 => 100' ) ; + is( $NUMBER_100, max( $NUMBER_100, $NUMBER_42, 1 ), 'max 100 42 1 => 100' ) ; + is( $NUMBER_100, max( $NUMBER_100, '42', 1 ), 'max 100 42 1 => 100' ) ; + is( $NUMBER_100, max( '100', '42', 1 ), 'max 100 42 1 => 100' ) ; + is( $NUMBER_100, max( $NUMBER_100, 'haha', 1 ), 'max 100 haha 1 => 100') ; + is( $NUMBER_100, max( 'bb', $NUMBER_100, 'haha' ), 'max bb 100 haha => 100') ; + is( $MINUS_ONE, max( q{}, $MINUS_ONE, 'haha' ), 'max "" -1 haha => -1') ; + is( $MINUS_ONE, max( q{}, $MINUS_ONE, $MINUS_TWO ), 'max "" -1 -2 => -1') ; + is( $MINUS_ONE, max( 'haha', $MINUS_ONE, $MINUS_TWO ), 'max haha -1 -2 => -1') ; + is( 1, max( $MINUS_ONE, 1 ), 'max -1 1 => 1') ; + is( 1, max( undef, 1 ), 'max undef 1 => 1' ) ; + is( 0, max( undef, 0 ), 'max undef 0 => 0' ) ; + is( 'haha', max( 'haha' ), 'max haha => haha') ; + is( 'bb', max( 'aa', 'bb' ), 'max aa bb => bb') ; + is( 'bb', max( 'bb', 'aa' ), 'max bb aa => bb') ; + is( 'bb', max( 'bb', 'aa', 'bb' ), 'max bb aa bb => bb') ; + note( 'Leaving tests_max()' ) ; + return ; +} + +sub max +{ + my @list = @_ ; + return( undef ) if ( 0 == scalar @list ) ; + + my( @numbers, @notnumbers ) ; + foreach my $item ( @list ) + { + if ( is_number( $item ) ) + { + push @numbers, $item ; + } + elsif ( defined $item ) + { + push @notnumbers, $item ; + } + } + + my @sorted ; + + if ( @numbers ) + { + @sorted = sort { $a <=> $b } @numbers ; + } + elsif ( @notnumbers ) + { + @sorted = sort { $a cmp $b } @notnumbers ; + } + else + { + return ; + } + + return( pop @sorted ) ; +} + +sub tests_is_number +{ + note( 'Entering tests_is_number()' ) ; + + is( undef, is_number( ), 'is_number: no args => undef ' ) ; + is( undef, is_number( undef ), 'is_number: undef => undef ' ) ; + ok( is_number( 1 ), 'is_number: 1 => 1' ) ; + ok( is_number( 1.1 ), 'is_number: 1.1 => 1' ) ; + ok( is_number( 0 ), 'is_number: 0 => 1' ) ; + ok( is_number( -1 ), 'is_number: -1 => 1' ) ; + ok( ! is_number( 1.1.1 ), 'is_number: 1.1.1 => no' ) ; + ok( ! is_number( q{} ), 'is_number: q{} => no' ) ; + ok( ! is_number( 'haha' ), 'is_number: haha => no' ) ; + ok( ! is_number( '0haha' ), 'is_number: 0haha => no' ) ; + ok( ! is_number( '2haha' ), 'is_number: 2haha => no' ) ; + ok( ! is_number( 'haha2' ), 'is_number: haha2 => no' ) ; + + note( 'Leaving tests_is_number()' ) ; + return ; +} + + + +sub is_number +{ + my $item = shift ; + + if ( ! defined $item ) { return ; } + + if ( $item =~ /\A$RE{num}{real}\Z/ ) { + return 1 ; + } + return ; +} + +sub tests_min +{ + note( 'Entering tests_min()' ) ; + + is( 0, min( 0 ), 'min 0 => 0' ) ; + is( 1, min( 1 ), 'min 1 => 1' ) ; + is( $MINUS_ONE, min( $MINUS_ONE ), 'min -1 => -1' ) ; + is( undef, min( ), 'min no arg => undef' ) ; + is( 1, min( 1, $NUMBER_100 ), 'min 1 100 => 1' ) ; + is( 1, min( $NUMBER_100, 1 ), 'min 100 1 => 1' ) ; + is( 1, min( $NUMBER_100, $NUMBER_42, 1 ), 'min 100 42 1 => 1' ) ; + is( 1, min( $NUMBER_100, '42', 1 ), 'min 100 42 1 => 1' ) ; + is( 1, min( '100', '42', 1 ), 'min 100 42 1 => 1' ) ; + is( 1, min( $NUMBER_100, 'haha', 1 ), 'min 100 haha 1 => 1') ; + is( $MINUS_ONE, min( $MINUS_ONE, 1 ), 'min -1 1 => -1') ; + + is( 1, min( undef, 1 ), 'min undef 1 => 1' ) ; + is( 0, min( undef, 0 ), 'min undef 0 => 0' ) ; + is( 1, min( undef, 1 ), 'min undef 1 => 1' ) ; + is( 0, min( undef, 2, 0, 1 ), 'min undef, 2, 0, 1 => 0' ) ; + + is( 'haha', min( 'haha' ), 'min haha => haha') ; + is( 'aa', min( 'aa', 'bb' ), 'min aa bb => aa') ; + is( 'aa', min( 'bb', 'aa' ), 'min bb aa bb => aa') ; + is( 'aa', min( 'bb', 'aa', 'bb' ), 'min bb aa bb => aa') ; + + note( 'Leaving tests_min()' ) ; + return ; +} + + +sub min +{ + my @list = @_ ; + return( undef ) if ( 0 == scalar @list ) ; + + my( @numbers, @notnumbers ) ; + foreach my $item ( @list ) { + if ( is_number( $item ) ) { + push @numbers, $item ; + }else{ + push @notnumbers, $item ; + } + } + + my @sorted ; + if ( @numbers ) { + @sorted = sort { $a <=> $b } @numbers ; + }elsif( @notnumbers ) { + @sorted = sort { $a cmp $b } @notnumbers ; + }else{ + return ; + } + + return( shift @sorted ) ; +} + + +sub check_lib_version +{ + my $mysync = shift ; + $mysync->{ debug } and myprint( "IMAPClient $Mail::IMAPClient::VERSION\n" ) ; + if ( '2.2.9' eq $Mail::IMAPClient::VERSION ) { + myprint( "imapsync no longer supports Mail::IMAPClient 2.2.9, upgrade it\n" ) ; + return 0 ; + } + else{ + # 3.x.x is no longer buggy with imapsync. + # 3.30 or currently superior is imposed in the Perl "use Mail::IMAPClient line". + return 1 ; + } + return ; +} + +sub module_version_str +{ + my( $module_name, $module_version ) = @_ ; + my $str = mysprintf( "%-20s %s\n", $module_name, $module_version ) ; + return( $str ) ; +} + +sub modulesversion +{ + + my @list_version; + + my %modulesversion = ( + 'Authen::NTLM' => sub { $Authen::NTLM::VERSION }, + 'CGI' => sub { $CGI::VERSION }, + 'Compress::Zlib' => sub { $Compress::Zlib::VERSION }, + 'Crypt::OpenSSL::RSA' => sub { $Crypt::OpenSSL::RSA::VERSION }, + 'Data::Uniqid' => sub { $Data::Uniqid::VERSION }, + 'Digest::HMAC_MD5' => sub { $Digest::HMAC_MD5::VERSION }, + 'Digest::HMAC_SHA1' => sub { $Digest::HMAC_SHA1::VERSION }, + 'Digest::MD5' => sub { $Digest::MD5::VERSION }, + 'Encode' => sub { $Encode::VERSION }, + 'Encode::IMAPUTF7' => sub { $Encode::IMAPUTF7::VERSION }, + 'File::Copy::Recursive' => sub { $File::Copy::Recursive::VERSION }, + 'File::Spec' => sub { $File::Spec::VERSION }, + 'Getopt::Long' => sub { $Getopt::Long::VERSION }, + 'HTML::Entities' => sub { $HTML::Entities::VERSION }, + 'IO::Socket' => sub { $IO::Socket::VERSION }, + 'IO::Socket::INET' => sub { $IO::Socket::INET::VERSION }, + 'IO::Socket::INET6' => sub { $IO::Socket::INET6::VERSION }, + 'IO::Socket::IP' => sub { $IO::Socket::IP::VERSION }, + 'IO::Socket::SSL' => sub { $IO::Socket::SSL::VERSION }, + 'IO::Tee' => sub { $IO::Tee::VERSION }, + 'JSON' => sub { $JSON::VERSION }, + 'JSON::WebToken' => sub { $JSON::WebToken::VERSION }, + 'LWP' => sub { $LWP::VERSION }, + 'Mail::IMAPClient' => sub { $Mail::IMAPClient::VERSION }, + 'MIME::Base64' => sub { $MIME::Base64::VERSION }, + 'Net::Ping' => sub { $Net::Ping::VERSION }, + 'Net::SSLeay' => sub { $Net::SSLeay::VERSION }, + 'Term::ReadKey' => sub { $Term::ReadKey::VERSION }, + 'Test::MockObject' => sub { $Test::MockObject::VERSION }, + 'Time::HiRes' => sub { $Time::HiRes::VERSION }, + 'Unicode::String' => sub { $Unicode::String::VERSION }, + 'URI::Escape' => sub { $URI::Escape::VERSION }, + #'Lalala' => sub { $Lalala::VERSION }, + ) ; + + foreach my $module_name ( sort keys %modulesversion ) { + # trick from http://www.perlmonks.org/?node_id=152122 + my $file_name = $module_name . '.pm' ; + $file_name =~s,::,/,xmgs; # Foo::Bar::Baz => Foo/Bar/Baz.pm + my $v ; + eval { + require $file_name ; + $v = defined $modulesversion{ $module_name } ? $modulesversion{ $module_name }->() : q{?} ; + } or $v = q{Not installed} ; + + push @list_version, module_version_str( $module_name, $v ) ; + } + return( @list_version ) ; +} + + +sub tests_command_line_nopassword +{ + note( 'Entering tests_command_line_nopassword()' ) ; + + ok( q{} eq command_line_nopassword(), 'command_line_nopassword void' ); + my $mysync = {} ; + ok( '--blabla' eq command_line_nopassword( $mysync, '--blabla' ), 'command_line_nopassword --blabla' ); + #myprint( command_line_nopassword((qw{ --password1 secret1 })), "\n" ) ; + ok( '--password1 MASKED' eq command_line_nopassword( $mysync, qw{ --password1 secret1}), 'command_line_nopassword --password1' ); + ok( '--blabla --password1 MASKED --blibli' + eq command_line_nopassword( $mysync, qw{ --blabla --password1 secret1 --blibli } ), 'command_line_nopassword --password1 --blibli' ); + $mysync->{showpasswords} = 1 ; + ok( q{} eq command_line_nopassword(), 'command_line_nopassword void' ); + ok( '--blabla' eq command_line_nopassword( $mysync, '--blabla'), 'command_line_nopassword --blabla' ); + #myprint( command_line_nopassword((qw{ --password1 secret1 })), "\n" ) ; + ok( '--password1 secret1' eq command_line_nopassword( $mysync, qw{ --password1 secret1} ), 'command_line_nopassword --password1' ); + ok( '--blabla --password1 secret1 --blibli' + eq command_line_nopassword( $mysync, qw{ --blabla --password1 secret1 --blibli } ), 'command_line_nopassword --password1 --blibli' ); + + note( 'Leaving tests_command_line_nopassword()' ) ; + return ; +} + +# Construct a command line copy with passwords replaced by MASKED. +sub command_line_nopassword +{ + my $mysync = shift @ARG ; + my @argv = @ARG ; + my @argv_nopassword ; + + if ( $mysync->{ cmdcgi } ) { + @argv_nopassword = mask_password_value( @{ $mysync->{ cmdcgi } } ) ; + return( "@argv_nopassword" ) ; + } + + if ( $mysync->{showpasswords} ) + { + return( "@argv" ) ; + } + + @argv_nopassword = mask_password_value( @argv ) ; + return("@argv_nopassword") ; +} + +sub mask_password_value +{ + my @argv = @ARG ; + my @argv_nopassword ; + while ( @argv ) { + my $arg = shift @argv ; # option name or value + if ( $arg =~ m/-password[12]/x ) { + shift @argv ; # password value + push @argv_nopassword, $arg, 'MASKED' ; # option name and fake value + }else{ + push @argv_nopassword, $arg ; # same option or value + } + } + return @argv_nopassword ; +} + + +sub tests_get_stdin_masked +{ + note( 'Entering tests_get_stdin_masked()' ) ; + + is( q{}, get_stdin_masked( ), 'get_stdin_masked: no args' ) ; + is( q{}, get_stdin_masked( 'Please ENTER: ' ), 'get_stdin_masked: ENTER' ) ; + + note( 'Leaving tests_get_stdin_masked()' ) ; + return ; +} + +####################################################### +# The issue is that prompt() does not prompt the prompt +# when the program is used like +# { sleep 2 ; echo blablabla ; } | ./imapsync ...--host1 lo --user1 tata --host2 lo --user2 titi + +# use IO::Prompter ; +sub get_stdin_masked +{ + my $prompt = shift || 'Say something: ' ; + local @ARGV = () ; + my $input = prompt( + -prompt => $prompt, + -echo => '*', + ) ; + #myprint( "You said: $input\n" ) ; + return $input ; +} + +sub ask_for_password_new +{ + my $prompt = shift ; + my $password = get_stdin_masked( $prompt ) ; + return $password ; +} +######################################################### + + +sub ask_for_password +{ + my $prompt = shift ; + myprint( $prompt ) ; + Term::ReadKey::ReadMode( 2 ) ; + ## no critic (InputOutput::ProhibitExplicitStdin) + my $password = ; + chomp $password ; + myprint( "\nGot it\n" ) ; + Term::ReadKey::ReadMode( 0 ) ; + return $password ; +} + +# Have to refactor get_password1() get_password2() +# to have only get_password() and two calls +sub get_password1 +{ + + my $mysync = shift ; + + $mysync->{ password1 } + || $mysync->{ passfile1 } + || 'PREAUTH' eq $mysync->{ acc1 }->{ authmech } + || 'EXTERNAL' eq $mysync->{ acc1 }->{ authmech } + || $ENV{IMAPSYNC_PASSWORD1} + || do + { + myprint( << 'FIN_PASSFILE' ) ; + +If you are afraid of giving password on the command line arguments, you can put the +password of user1 in a file named file1 and use "--passfile1 file1" instead of typing it. +Then give this file restrictive permissions with the command "chmod 600 file1". +An other solution is to set the environment variable IMAPSYNC_PASSWORD1 +FIN_PASSFILE + my $user = $mysync->{ acc1 }->{ authuser } || $mysync->{ user1 } ; + my $host = $mysync->{ host1 } ; + my $prompt = "What's the password for $user" . ' at ' . "$host? (not visible while you type, then enter RETURN) " ; + $mysync->{password1} = ask_for_password( $prompt ) ; + } ; + + if ( defined $mysync->{ passfile1 } ) { + if ( ! -e -r $mysync->{ passfile1 } ) { + myprint( "Failure: file from parameter --passfile1 $mysync->{ passfile1 } does not exist or is not readable\n" ) ; + $mysync->{nb_errors}++ ; + exit_clean( $mysync, $EX_NOINPUT ) ; + } + # passfile1 readable + $mysync->{password1} = firstline ( $mysync->{ passfile1 } ) ; + return ; + } + if ( $ENV{IMAPSYNC_PASSWORD1} ) { + $mysync->{password1} = $ENV{IMAPSYNC_PASSWORD1} ; + return ; + } + return ; +} + +sub get_password2 +{ + + my $mysync = shift ; + + $mysync->{password2} + || $mysync->{ passfile2 } + || 'PREAUTH' eq $mysync->{ acc2 }->{ authmech } + || 'EXTERNAL' eq $mysync->{ acc2 }->{ authmech } + || $ENV{IMAPSYNC_PASSWORD2} + || do + { + myprint( << 'FIN_PASSFILE' ) ; + +If you are afraid of giving password on the command line arguments, you can put the +password of user2 in a file named file2 and use "--passfile2 file2" instead of typing it. +Then give this file restrictive permissions with the command "chmod 600 file2". +An other solution is to set the environment variable IMAPSYNC_PASSWORD2 +FIN_PASSFILE + my $user = $mysync->{ acc2 }->{ authuser } || $mysync->{ user2 } ; + my $host = $mysync->{ host2 } ; + my $prompt = "What's the password for $user" . ' at ' . "$host? (not visible while you type, then enter RETURN) " ; + $mysync->{password2} = ask_for_password( $prompt ) ; + } ; + + + if ( defined $mysync->{ passfile2 } ) { + if ( ! -e -r $mysync->{ passfile2 } ) { + myprint( "Failure: file from parameter --passfile2 $mysync->{ passfile2 } does not exist or is not readable\n" ) ; + $mysync->{nb_errors}++ ; + exit_clean( $mysync, $EX_NOINPUT ) ; + } + # passfile2 readable + $mysync->{password2} = firstline ( $mysync->{ passfile2 } ) ; + return ; + } + if ( $ENV{IMAPSYNC_PASSWORD2} ) { + $mysync->{password2} = $ENV{IMAPSYNC_PASSWORD2} ; + return ; + } + return ; +} + + + + +sub remove_tmp_files +{ + my $mysync = shift or return ; + $mysync->{pidfile} or return ; + + if ( -e $mysync->{pidfile} ) { + myprint( "Removing pidfile $mysync->{pidfile}\n" ) ; + unlink $mysync->{pidfile} ; + } + if ( -e $mysync->{abortfile} ) { + myprint( "Removing pidfile $mysync->{abortfile}\n" ) ; + unlink $mysync->{abortfile} ; + } + return ; +} + +sub cleanup_before_exit +{ + my $mysync = shift ; + + remove_tmp_files( $mysync ) ; + + if ( $mysync->{imap1} and $mysync->{imap1}->IsConnected() ) + { + myprint( "Disconnecting from host1 $mysync->{ host1 } user1 $mysync->{ user1 }\n" ) ; + $mysync->{imap1}->logout( ) ; + } + + if ( $mysync->{imap2} and $mysync->{imap2}->IsConnected() ) + { + myprint( "Disconnecting from host2 $mysync->{ host2 } user2 $mysync->{ user2 }\n" ) ; + $mysync->{imap2}->logout( ) ; + } + + if ( $mysync->{log} ) { + myprint( "Log file is $mysync->{logfile} ( to change it, use --logfile filepath ; or use --nolog to turn off logging )\n" ) ; + } + else + { + myprint( "No log file because of option --nolog\n" ) ; + } + + if ( $mysync->{log} and $mysync->{logfile_handle} ) { + #print( "Closing $mysync->{ logfile }\n" ) ; + teefinish( $mysync ) ; + } + + return ; +} + + +sub tests_exit_value +{ + note( 'Entering tests_exit_value()' ) ; + + is( $EXIT_CATCH_ALL, exit_value( ), 'exit_value: no args => EXIT_CATCH_ALL' ) ; + + my $mysync = { } ; + is( $EXIT_CATCH_ALL, exit_value( $mysync ), 'exit_value: undef => EXIT_CATCH_ALL' ) ; + + is( $EXIT_CATCH_ALL, exit_value( $mysync, 'Blabla_unknown' ), 'exit_value: Blabla => EXIT_CATCH_ALL' ) ; + is( $EXIT_CATCH_ALL, exit_value( $mysync, '' ), 'exit_value: empty => EXIT_CATCH_ALL' ) ; + + + is( $EXIT_OVERQUOTA, exit_value( $mysync, 'ERR_OVERQUOTA' ), 'exit_value: ERR_OVERQUOTA => EXIT_OVERQUOTA' ) ; + is( $EXIT_TRANSFER_EXCEEDED, exit_value( $mysync, 'ERR_TRANSFER_EXCEEDED' ), 'exit_value: ERR_TRANSFER_EXCEEDED => EXIT_TRANSFER_EXCEEDED' ) ; + + note( 'Leaving tests_exit_value()' ) ; + return ; +} + +sub exit_value +{ + my $mysync = shift @ARG ; + my $most_common_error = shift @ARG ; + + if ( ! defined $most_common_error ) { return $EXIT_CATCH_ALL ; } + my $exit_value = $EXIT_VALUE_OF_ERR_TYPE{ $most_common_error } || $EXIT_CATCH_ALL ; + + return $exit_value ; +} + + + +sub exit_most_errors +{ + my $mysync = shift @ARG ; + + myprint( errors_listing( $mysync ) ) ; + my $exit_value = exit_value( $mysync, $mysync->{ most_common_error } ) ; + exit_clean( $mysync, $exit_value ) ; + return ; +} + +sub exit_clean +{ + my $mysync = shift @ARG ; + my $status = shift @ARG ; + my @messages = @ARG ; + if ( @messages ) + { + myprint( @messages ) ; + } + myprint( "Exiting with return value $status ($EXIT_TXT{$status}) $mysync->{nb_errors}/$mysync->{errorsmax} nb_errors/max_errors PID $PROCESS_ID\n" ) ; + cleanup_before_exit( $mysync ) ; + + exit $status ; +} + +sub missing_option +{ + my $mysync = shift ; + my $option = shift ; + $mysync->{nb_errors}++ ; + exit_clean( $mysync, $EX_USAGE, "$option option is mandatory, for help run $PROGRAM_NAME --help\n" ) ; + return ; +} + + +sub catch_ignore +{ + my $mysync = shift ; + my $signame = shift ; + + my $sigcounter = ++$mysync->{ sigcounter }{ $signame } ; + myprint( "\nGot a signal $signame (my PID is $PROCESS_ID my PPID is ", getppid( ), + "). Received $sigcounter $signame signals so far. Thanks!\n" ) ; + do_and_print_stats( $mysync ) ; + return ; +} + + + +sub catch_exit +{ + my $mysync = shift ; + my $signame = shift || q{} ; + if ( $signame ) { + myprint( "\nGot a signal $signame (my PID is $PROCESS_ID my PPID is ", getppid( ), + "). Asked to terminate\n" ) ; + if ( $mysync->{can_do_stats} ) { + do_and_print_stats( $mysync ) ; + myprint( "Ended by a signal $signame (my PID is $PROCESS_ID my PPID is ", + getppid( ), "). I am asked to terminate immediately.\n" ) ; + } + myprint( "You should resynchronize those accounts by running a sync again,\n", + "since some messages and entire folders might still be missing on host2.\n" + ) ; + ## no critic (RequireLocalizedPunctuationVars) + # Well, restore default action does not work well + $SIG{ $signame } = 'DEFAULT'; # restore default action + #$SIG{ 'TERM' } = 'DEFAULT'; # restore default action + # kill myself with $signame + # https://www.cons.org/cracauer/sigint.html + myprint( "Killing myself with signal $signame\n" ) ; + #cleanup_before_exit( $mysync ) ; + kill( $signame, $PROCESS_ID ) ; + #kill( 'TERM', $PROCESS_ID ) ; + #sleep 1 ; + #while ( 1 ) { } ; + $mysync->{nb_errors}++ ; + exit_clean( $mysync, $EXIT_BY_SIGNAL, + "Still there after killing myself with signal $signame...\n" + ) ; + } + else + { + $mysync->{nb_errors}++ ; + exit_clean( $mysync, $EXIT_BY_SIGNAL, "Exiting in catch_exit with no signal...\n" ) ; + } + return ; +} + + +sub catch_print +{ + my $mysync = shift ; + my $signame = shift ; + + my $sigcounter = ++$mysync->{ sigcounter }{ $signame } ; + myprint( "\nGot a signal $signame (my PID is $PROCESS_ID my PPID is ", getppid( ), + "). Received $sigcounter $signame signals so far. Thanks!\n" ) ; + return ; +} + +sub here_twice +{ + my $mysync = shift ; + my $now = time ; + my $previous = $mysync->{lastcatch} || 0 ; + $mysync->{lastcatch} = $now ; + + if ( $INTERVAL_TO_EXIT >= $now - $previous ) { + return $TRUE ; + }else{ + return $FALSE ; + } +} + + +sub catch_reconnect +{ + my $mysync = shift ; + my $signame = shift ; + if ( here_twice( $mysync ) ) { + myprint( "Got two signals $signame within $INTERVAL_TO_EXIT seconds. Exiting...\n" ) ; + catch_exit( $mysync, $signame ) ; + }else{ + myprint( "\nGot a signal $signame (my PID is $PROCESS_ID my PPID is ", getppid( ), ")\n", + "Hit 2 ctr-c within 2 seconds to exit the program\n", + "Hit only 1 ctr-c to reconnect to both imap servers\n", + ) ; + myprint( "For now only one signal $signame within $INTERVAL_TO_EXIT seconds.\n" ) ; + + if ( ! defined $mysync->{imap1} ) { return ; } + if ( ! defined $mysync->{imap2} ) { return ; } + + myprint( "Info: reconnecting to host1 imap server $mysync->{host1}\n" ) ; + $mysync->{imap1}->State( Mail::IMAPClient::Unconnected ) ; + $mysync->{imap1}->{IMAPSYNC_RECONNECT_COUNT} += 1 ; + if ( $mysync->{imap1}->reconnect( ) ) + { + myprint( "Info: reconnected to host1 imap server $mysync->{host1}\n" ) ; + } + else + { + $mysync->{nb_errors}++ ; + exit_clean( $mysync, $EXIT_CONNECTION_FAILURE ) ; + } + myprint( "Info: reconnecting to host2 imap server\n" ) ; + $mysync->{imap2}->State( Mail::IMAPClient::Unconnected ) ; + $mysync->{imap2}->{IMAPSYNC_RECONNECT_COUNT} += 1 ; + if ( $mysync->{imap2}->reconnect( ) ) + { + myprint( "Info: reconnected to host2 imap server $mysync->{host2}\n" ) ; + } + else + { + $mysync->{nb_errors}++ ; + exit_clean( $mysync, $EXIT_CONNECTION_FAILURE ) ; + } + myprint( "Info: reconnected to both imap servers\n" ) ; + } + return ; +} + +sub install_signals +{ + my $mysync = shift ; + + if ( under_docker_context( $mysync ) ) + { + # output( $mysync, "Under docker context so leaving signals as they are\n" ) ; + output( $mysync, "Under docker context so installing only signals to exit\n" ) ; + @{ $mysync->{ sigexit } } = ( defined( $mysync->{ sigexit } ) ) ? @{ $mysync->{ sigexit } } : ( 'INT', 'QUIT', 'TERM' ) ; + sig_install( $mysync, 'catch_exit', @{ $mysync->{ sigexit } } ) ; + } + else + { + # Unix signals + @{ $mysync->{ sigexit } } = ( defined( $mysync->{ sigexit } ) ) ? @{ $mysync->{ sigexit } } : ( 'QUIT', 'TERM' ) ; + @{ $mysync->{ sigreconnect } } = ( defined( $mysync->{ sigreconnect } ) ) ? @{ $mysync->{ sigreconnect } } : ( 'INT' ) ; + @{ $mysync->{ sigprint } } = ( defined( $mysync->{ sigprint } ) ) ? @{ $mysync->{ sigprint } } : ( 'HUP' ) ; + @{ $mysync->{ sigignore } } = ( defined( $mysync->{ sigignore } ) ) ? @{ $mysync->{ sigignore } } : ( ) ; + + #local %SIG = %SIG ; + sig_install( $mysync, 'catch_exit', @{ $mysync->{ sigexit } } ) ; + sig_install( $mysync, 'catch_reconnect', @{ $mysync->{ sigreconnect } } ) ; + sig_install( $mysync, 'catch_print', @{ $mysync->{ sigprint } } ) ; + # --sigignore can override sigexit, sigreconnect and sigprint (for the same signals only) + sig_install( $mysync, 'catch_ignore', @{ $mysync->{ sigignore } } ) ; + + # remove/add sleeping mechanism when receiving USR1 signal (except on Win32) + sig_install_toggle_sleep( $mysync ) ; + } + + return ; +} + + + +sub tests_reconnect_12_if_needed +{ + note( 'Entering tests_reconnect_12_if_needed()' ) ; + + my $mysync ; + + $mysync->{imap1} = Mail::IMAPClient->new( ) ; + $mysync->{imap2} = Mail::IMAPClient->new( ) ; + $mysync->{imap1}->Server( 'test1.lamiral.info' ) ; + $mysync->{imap2}->Server( 'test2.lamiral.info' ) ; + is( 2, reconnect_12_if_needed( $mysync ), 'reconnect_12_if_needed: test1&test2 .lamiral.info => 1' ) ; + is( 1, $mysync->{imap1}->{IMAPSYNC_RECONNECT_COUNT}, 'reconnect_12_if_needed: test1.lamiral.info IMAPSYNC_RECONNECT_COUNT => 1' ) ; + is( 1, $mysync->{imap2}->{IMAPSYNC_RECONNECT_COUNT}, 'reconnect_12_if_needed: test2.lamiral.info IMAPSYNC_RECONNECT_COUNT => 1' ) ; + + note( 'Leaving tests_reconnect_12_if_needed()' ) ; + return ; +} + +sub reconnect_12_if_needed +{ + my $mysync = shift ; + #return 2 ; + if ( ! reconnect_if_needed( $mysync->{imap1} ) ) { + return ; + } + if ( ! reconnect_if_needed( $mysync->{imap2} ) ) { + return ; + } + # both were good + return 2 ; +} + + +sub tests_reconnect_if_needed +{ + note( 'Entering tests_reconnect_if_needed()' ) ; + + + my $myimap ; + + is( undef, reconnect_if_needed( ), 'reconnect_if_needed: no args => undef' ) ; + is( undef, reconnect_if_needed( $myimap ), 'reconnect_if_needed: undef arg => undef' ) ; + + $myimap = Mail::IMAPClient->new( ) ; + $myimap->Debug( 1 ) ; + is( undef, reconnect_if_needed( $myimap ), 'reconnect_if_needed: empty new Mail::IMAPClient => undef' ) ; + $myimap->Server( 'test.lamiral.info' ) ; + is( 1, reconnect_if_needed( $myimap ), 'reconnect_if_needed: test.lamiral.info => 1' ) ; + is( 1, $myimap->{IMAPSYNC_RECONNECT_COUNT}, 'reconnect_if_needed: test.lamiral.info IMAPSYNC_RECONNECT_COUNT => 1' ) ; + + note( 'Leaving tests_reconnect_if_needed()' ) ; + return ; +} + +sub reconnect_if_needed +{ + # return undef upon failure. + # return 1 upon connection success, with or without reconnection. + + my $imap = shift ; + + if ( ! defined $imap ) { return ; } + if ( ! $imap->Server( ) ) { return ; } + + if ( $imap->IsUnconnected( ) ) { + $imap->{IMAPSYNC_RECONNECT_COUNT} += 1 ; + if ( $imap->reconnect( ) ) { + return 1 ; + } + }else{ + return 1 ; + } + + # A last forced one + $imap->State( Mail::IMAPClient::Unconnected ) ; + $imap->reconnect( ) ; + $imap->{IMAPSYNC_RECONNECT_COUNT} += 1 ; + if ( $imap->noop ) { + # NOOP is ok + return 1 ; + } + + return ; +} + + +sub justconnect +{ + my $mysync = shift ; + my $justconnect1 = justconnect1( $sync ) ; + my $justconnect2 = justconnect2( $sync ) ; + return "$justconnect1 $justconnect2"; +} + +sub justconnect1 +{ + my $mysync = shift ; + if ( $mysync->{host1} ) + { + myprint( "Host1: Will just connect to $mysync->{host1} without login\n" ) ; + $mysync->{imap1} = connect_imap( + $mysync->{host1}, $mysync->{port1}, + $mysync->{ssl1}, $mysync->{tls1}, + $mysync->{ acc1 } ) ; + + imap_id( $mysync, $mysync->{imap1}, $mysync->{ acc1 }->{ Side } ) ; + $mysync->{imap1}->logout( ) ; + return $mysync->{host1} ; + } + + return q{} ; +} + +sub justconnect2 +{ + my $mysync = shift ; + if ( $mysync->{host2} ) + { + myprint( "Host2: Will just connect to $mysync->{host2} without login\n" ) ; + $mysync->{imap2} = connect_imap( + $mysync->{host2}, $mysync->{port2}, + $mysync->{ssl2}, $mysync->{tls2}, + $mysync->{ acc2 } ) ; + + imap_id( $mysync, $mysync->{imap2}, $mysync->{ acc2 }->{ Side } ) ; + $mysync->{imap2}->logout( ) ; + return $mysync->{host2} ; + } + + return q{} ; +} + +sub skip_macosx +{ + #return ; + # hostname is sometimes "macosx.polarhome.com" sometimes "macosx" + return( ( ( 'macosx.polarhome.com' eq hostname( ) ) || ( 'macosx' eq hostname( ) ) ) + && ( 'darwin' eq $OSNAME ) ) ; +} + +sub skip_macosx_binary +{ + #return ; + return( skip_macosx( ) && ( $PROGRAM_NAME =~ m{imapsync_bin_Darwin} ) ) ; +} + + + +sub tests_mailimapclient_connect +{ + note( 'Entering tests_mailimapclient_connect()' ) ; + + my $imap ; + # ipv4 + ok( $imap = Mail::IMAPClient->new( ), 'mailimapclient_connect ipv4: new' ) ; + is( 'Mail::IMAPClient', ref( $imap ), 'mailimapclient_connect ipv4: ref is Mail::IMAPClient' ) ; + + # Mail::IMAPClient 3.40 die on this... So we skip it, thanks to "mature" IO::Socket::IP + # Mail::IMAPClient 3.42 is ok so this test is back. + is( undef, $imap->connect( ), 'mailimapclient_connect ipv4: connect with no server => failure' ) ; + + + is( 'test.lamiral.info', $imap->Server( 'test.lamiral.info' ), 'mailimapclient_connect ipv4: setting Server(test.lamiral.info)' ) ; + is( 1, $imap->Debug( 1 ), 'mailimapclient_connect ipv4: setting Debug( 1 )' ) ; + is( 143, $imap->Port( 143 ), 'mailimapclient_connect ipv4: setting Port( 143 )' ) ; + is( 10, $imap->Timeout( 10 ), 'mailimapclient_connect ipv4: setting Timeout( 10 )' ) ; + like( ref( $imap->connect( ) ), qr/IO::Socket::INET|IO::Socket::IP/, 'mailimapclient_connect ipv4: connect to test.lamiral.info' ) ; + like( $imap->logout( ), qr/Mail::IMAPClient/, 'mailimapclient_connect ipv4: logout' ) ; + is( undef, undef $imap, 'mailimapclient_connect ipv4: free variable' ) ; + + # ipv4 + ssl + ok( $imap = Mail::IMAPClient->new( ), 'mailimapclient_connect ipv4 + ssl: new' ) ; + is( 'test.lamiral.info', $imap->Server( 'test.lamiral.info' ), 'mailimapclient_connect ipv4 + ssl: setting Server(test.lamiral.info)' ) ; + is( 1, $imap->Debug( 1 ), 'mailimapclient_connect ipv4 + ssl: setting Debug( 1 )' ) ; + ok( $imap->Ssl( [ SSL_verify_mode => SSL_VERIFY_NONE, SSL_cipher_list => 'DEFAULT:!DH' ] ), 'mailimapclient_connect ipv4 + ssl: setting Ssl( SSL_VERIFY_NONE )' ) ; + is( 993, $imap->Port( 993 ), 'mailimapclient_connect ipv4 + ssl: setting Port( 993 )' ) ; + like( ref( $imap->connect( ) ), qr/IO::Socket::SSL/, 'mailimapclient_connect ipv4 + ssl: connect to test.lamiral.info' ) ; + like( $imap->logout( ), qr/Mail::IMAPClient/, 'mailimapclient_connect ipv4 + ssl: logout in ssl does not cause failure' ) ; + is( undef, undef $imap, 'mailimapclient_connect ipv4 + ssl: free variable' ) ; + + # ipv6 + ssl + + ok( $imap = Mail::IMAPClient->new( ), 'mailimapclient_connect ipv6 + ssl: new' ) ; + is( 'petiteipv6.lamiral.info', $imap->Server( 'petiteipv6.lamiral.info' ), 'mailimapclient_connect ipv6 + ssl: setting Server petiteipv6.lamiral.info' ) ; + is( 10, $imap->Timeout( 10 ), 'mailimapclient_connect ipv6: setting Timeout( 10 )' ) ; + ok( $imap->Ssl( [ SSL_verify_mode => SSL_VERIFY_NONE, SSL_cipher_list => 'DEFAULT:!DH' ] ), 'mailimapclient_connect ipv6 + ssl: setting Ssl( SSL_VERIFY_NONE )' ) ; + is( 993, $imap->Port( 993 ), 'mailimapclient_connect ipv6 + ssl: setting Port( 993 )' ) ; + SKIP: { + if ( + 'CUILLERE' eq hostname() + or + skip_macosx() + or + -e '/.dockerenv' + or + 'pcHPDV7-HP' eq hostname() + ) + { + skip( 'Tests avoided on CUILLERE/pcHPDV7-HP/macosx.polarhome.com/docker cannot do ipv6', 4 ) ; + } + + is( 1, $imap->Debug( 1 ), 'mailimapclient_connect ipv4 + ssl: setting Debug( 1 )' ) ; + + # It sounds stupid but it avoids failures on the next test about $imap->connect + is( '2a01:e34:ecde:70d0:223:54ff:fec2:36d7', resolv( 'petiteipv6.lamiral.info' ), 'resolv: petiteipv6.lamiral.info => 2a01:e34:ecde:70d0:223:54ff:fec2:36d7' ) ; + + like( ref( $imap->connect( ) ), qr/IO::Socket::SSL/, 'mailimapclient_connect ipv6 + ssl: connect to petiteipv6.lamiral.info' ) ; + # This one is ok on petite, not on ks2, do not know why, so commented. + like( ref( $imap->logout( ) ), qr/Mail::IMAPClient/, 'mailimapclient_connect ipv6 + ssl: logout in ssl is ok on petiteipv6.lamiral.info' ) ; + } + + is( undef, undef $imap, 'mailimapclient_connect ipv6 + ssl: free variable' ) ; + + + note( 'Leaving tests_mailimapclient_connect()' ) ; + return ; +} + + +sub tests_mailimapclient_connect_bug +{ + note( 'Entering tests_mailimapclient_connect_bug()' ) ; + + my $imap ; + + # ipv6 + ok( $imap = Mail::IMAPClient->new( ), 'mailimapclient_connect_bug ipv6: new' ) ; + is( 'ks6ipv6.lamiral.info', $imap->Server( 'ks6ipv6.lamiral.info' ), 'mailimapclient_connect_bug ipv6: setting Server(ks6ipv6.lamiral.info)' ) ; + is( 143, $imap->Port( 143 ), 'mailimapclient_connect_bug ipv6: setting Port( 993 )' ) ; + + SKIP: { + if ( + 'CUILLERE' eq hostname() + or + skip_macosx() + or + -e '/.dockerenv' + or + 'pcHPDV7-HP' eq hostname() + ) + { + skip( 'Tests avoided on CUILLERE/pcHPDV7-HP/macosx.polarhome.com/docker cannot do ipv6', 1 ) ; + } + like( ref( $imap->connect( ) ), qr/IO::Socket::INET/, 'mailimapclient_connect_bug ipv6: connect to ks6ipv6.lamiral.info' ) + or diag( 'mailimapclient_connect_bug ipv6: ', $imap->LastError( ), $!, ) ; + } + #is( $imap->logout( ), undef, 'mailimapclient_connect_bug ipv6: logout in ssl causes failure' ) ; + is( undef, undef $imap, 'mailimapclient_connect_bug ipv6: free variable' ) ; + + note( 'Leaving tests_mailimapclient_connect_bug()' ) ; + return ; +} + + + +sub tests_connect_socket +{ + note( 'Entering tests_connect_socket()' ) ; + + is( undef, connect_socket( ), 'connect_socket: no args' ) ; + + my $socket ; + my $imap ; + SKIP: { + if ( + 'CUILLERE' eq hostname() + or + skip_macosx() + or + -e '/.dockerenv' + or + 'pcHPDV7-HP' eq hostname() + ) + { + skip( 'Tests avoided on CUILLERE/pcHPDV7-HP/macosx.polarhome.com/docker cannot do ipv6', 2 ) ; + } + + $socket = IO::Socket::INET6->new( + PeerAddr => 'ks6ipv6.lamiral.info', + PeerPort => 143, + ) ; + + + ok( $imap = connect_socket( $socket ), 'connect_socket: ks6ipv6.lamiral.info port 143 IO::Socket::INET6' ) ; + #$imap->Debug( 1 ) ; + # myprint( $imap->capability( ) ) ; + if ( $imap ) { + $imap->logout( ) ; + } + + $IO::Socket::SSL::DEBUG = 4 ; + $socket = IO::Socket::SSL->new( + PeerHost => 'ks6ipv6.lamiral.info', + PeerPort => 993, + SSL_verify_mode => SSL_VERIFY_NONE, + SSL_cipher_list => 'DEFAULT:!DH', + ) ; + # myprint( $socket ) ; + ok( $imap = connect_socket( $socket ), 'connect_socket: ks6ipv6.lamiral.info port 993 IO::Socket::SSL' ) ; + #$imap->Debug( 1 ) ; + # myprint( $imap->capability( ) ) ; + # $socket->close( ) ; + if ( $imap ) { + $socket->close( ) ; + } + #$socket->close(SSL_no_shutdown => 1) ; + #$imap->logout( ) ; + #myprint( "\n" ) ; + #$imap->logout( ) ; + } + note( 'Leaving tests_connect_socket()' ) ; + return ; +} + +sub connect_socket +{ + my( $socket ) = @ARG ; + + if ( ! defined $socket ) { return ; } + + my $host = $socket->peerhost( ) ; + my $port = $socket->peerport( ) ; + #print "socket->peerhost: ", $socket->peerhost( ), "\n" ; + #print "socket->peerport: ", $socket->peerport( ), "\n" ; + my $imap = Mail::IMAPClient->new( ) ; + $imap->Socket( $socket ) ; + my $banner = $imap->Results()->[0] ; + #myprint( "banner: $banner" ) ; + return $imap ; +} + + +sub tests_probe_imapssl +{ + note( 'Entering tests_probe_imapssl()' ) ; + + is( undef, probe_imapssl( ), 'probe_imapssl: no args => undef' ) ; + is( undef, probe_imapssl( 'unknown' ), 'probe_imapssl: unknown => undef' ) ; + + note( "hostname is: ", hostname() ) ; + SKIP: { + if ( + 'CUILLERE' eq hostname() + or + skip_macosx() + or + -e '/.dockerenv' + or + 'pcHPDV7-HP' eq hostname() + ) + { + skip( 'Tests avoided on CUILLERE or pcHPDV7-HP or Mac or docker: cannot do ipv6', 0 ) ; + } + # fed up with this one + #like( probe_imapssl( 'ks6ipv6.lamiral.info' ), qr/^\* OK/, 'probe_imapssl: ks6ipv6.lamiral.info matches "* OK"' ) ; + } ; + + + # It sounds stupid but it avoids failures on the next test about $imap->connect + ok( resolv( 'imap.gmail.com' ), 'resolv: imap.gmail.com => something' ) ; + like( probe_imapssl( 'imap.gmail.com' ), qr/^\* OK/, 'probe_imapssl: imap.gmail.com matches "* OK"' ) ; + + like( probe_imapssl( 'test1.lamiral.info' ), qr/^\* OK/, 'probe_imapssl: test1.lamiral.info matches "* OK"' ) ; + + note( 'Leaving tests_probe_imapssl()' ) ; + return ; +} + + +sub probe_imapssl +{ + my $host = shift ; + + if ( ! $host ) { return ; } + $sync->{ debug } and $IO::Socket::SSL::DEBUG = 4 ; + my $socket = IO::Socket::SSL->new( + PeerHost => $host, + PeerPort => $IMAP_SSL_PORT, + SSL_verifycn_scheme => 'imap', + SSL_verify_mode => $SSL_VERIFY_POLICY, + SSL_cipher_list => 'DEFAULT:!DH', + ) ; + if ( ! $socket ) { return ; } + $sync->{ debug } and print "socket: $socket\n" ; + + my $banner ; + $socket->sysread( $banner, 65_536 ) ; + $sync->{ debug } and print "banner: $banner" ; + $socket->close( ) ; + return $banner ; + +} + +sub connect_imap +{ + my( $host, $port, $ssl, $tls, $acc ) = @_ ; + my $imap = Mail::IMAPClient->new( ) ; + + if ( $ssl ) { set_ssl( $imap, $acc ) } + $imap->Server( $host ) ; + $imap->Port( $port ) ; + $imap->Debug( $acc->{ debugimap } ) ; + $imap->Timeout( $acc->{ timeout } ) ; + + #$imap->Keepalive( $acc->{ keepalive } ) ; + + + my $side = lc $acc->{ Side } ; + + myprint( "$acc->{ Side }: connecting on $side [$host] port [$port]\n" ) ; + + if ( ! $imap->connect( ) ) + { + $sync->{nb_errors}++ ; + exit_clean( $sync, $EXIT_CONNECTION_FAILURE, + "$acc->{ Side }: Can not open imap connection on [$host]: ", + $imap->LastError, + " $OS_ERROR\n" + ) ; + } + myprint( "$acc->{ Side } IP address: ", $imap->Socket->peerhost(), "\n" ) ; + + my $banner = $imap->Results()->[0] ; + + myprint( "$acc->{ Side } banner: $banner" ) ; + myprint( "$acc->{ Side } capability: ", join(q{ }, @{ $imap->capability() || [] }), "\n" ) ; + + if ( $tls ) { + set_tls( $imap, $acc ) ; + if ( ! $imap->starttls( ) ) + { + $sync->{nb_errors}++ ; + exit_clean( $sync, $EXIT_TLS_FAILURE, + "$acc->{ Side }: Can not go to tls encryption on $side [$host]:", + $imap->LastError, "\n" + ) ; + } + myprint( "$acc->{ Side }: Socket successfully converted to SSL\n" ) ; + } + return( $imap ) ; +} + +sub tests_compress_ssl +{ + note( 'Entering tests_compress_ssl()' ) ; + + SKIP: { + if ( skip_macosx( ) ) + { + skip( 'Tests avoided on host polarhome macosx, no clue "ssl3_get_server_certificate:certificate verify failed"', 12 ) ; + } + else + { + my $myimap ; + my $acc = {} ; + $acc->{ Side } = 'HostK' ; + $acc->{ authmech } = 'LOGIN' ; + $acc->{ debugimap } = 1 ; + $acc->{ compress } = 1 ; + $acc->{ N } = 'K' ; + + ok( + $myimap = login_imap( 'test1.lamiral.info', 993, 'test1', 'secret1', + 1, undef, + 1, 100, $acc, {}, + ), 'acc_compress_imap: test1.lamiral.info test1 ssl' ) ; + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'acc_compress_imap: test1.lamiral.info test1 ssl IsAuthenticated' ) ; + + + is( $acc->{ imap }, acc_compress_imap( $acc ), "acc_compress_imap: test1.lamiral.info ok" ) ; + is( undef, acc_compress_imap( $acc ), "acc_compress_imap: test1.lamiral.info 2nd nok" ) ; + + ok( + $myimap = login_imap( 'test1.lamiral.info', 143, 'test1', 'secret1', + 0, undef, + 1, 100, $acc, {}, + ), 'acc_compress_imap: test1.lamiral.info test1 tls' ) ; + ok( $myimap && $myimap->IsAuthenticated( ), 'acc_compress_imap: test1.lamiral.info test1 tls IsAuthenticated' ) ; + + is( $acc->{ imap }, acc_compress_imap( $acc ), "acc_compress_imap: test1.lamiral.info tls ok" ) ; + is( undef, acc_compress_imap( $acc ), "acc_compress_imap: test1.lamiral.info tls 2nd nok" ) ; + + # Third, no compression + $acc->{ compress } = 0 ; + ok( + $myimap = login_imap( 'test1.lamiral.info', 993, 'test1', 'secret1', + 1, undef, + 1, 100, $acc, {}, + ), 'acc_compress_imap: test1.lamiral.info test1 ssl' ) ; + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'acc_compress_imap: test1.lamiral.info test1 ssl IsAuthenticated' ) ; + + + is( undef, acc_compress_imap( $acc ), "acc_compress_imap: test1.lamiral.info off ok" ) ; + is( undef, acc_compress_imap( $acc ), "acc_compress_imap: test1.lamiral.info 2nd off ok" ) ; + + } + } + note( 'Leaving tests_compress_ssl()' ) ; + return ; +} + +sub tests_compress +{ + note( 'Entering tests_compress()' ) ; + + my $myimap ; + my $acc = {} ; + $acc->{ Side } = 'HostK' ; + $acc->{ authmech } = 'LOGIN' ; + $acc->{ debugimap } = 1 ; + $acc->{ compress } = 1 ; + $acc->{ N } = 'K' ; + + ok( + $myimap = login_imap( 'test1.lamiral.info', 143, 'test1', 'secret1', + 0, 0, + 1, 100, $acc, {}, + ), 'acc_compress_imap: test1.lamiral.info test1' ) ; + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'acc_compress_imap: test1.lamiral.info test1 IsAuthenticated' ) ; + + + is( $acc->{ imap }, acc_compress_imap( $acc ), "acc_compress_imap: test1.lamiral.info ok" ) ; + is( undef, acc_compress_imap( $acc ), "acc_compress_imap: test1.lamiral.info 2nd nok" ) ; + + ok( + $myimap = login_imap( 'test1.lamiral.info', 143, 'test1', 'secret1', + 0, 0, + 1, 100, $acc, {}, + ), 'acc_compress_imap: test1.lamiral.info test1 tls' ) ; + ok( $myimap && $myimap->IsAuthenticated( ), 'acc_compress_imap: test1.lamiral.info test1 tls IsAuthenticated' ) ; + + is( $acc->{ imap }, acc_compress_imap( $acc ), "acc_compress_imap: test1.lamiral.info tls ok" ) ; + is( undef, acc_compress_imap( $acc ), "acc_compress_imap: test1.lamiral.info tls 2nd nok" ) ; + + # Third, no compression + $acc->{ compress } = 0 ; + ok( + $myimap = login_imap( 'test1.lamiral.info', 143, 'test1', 'secret1', + 0, 0, + 1, 100, $acc, {}, + ), 'acc_compress_imap: test1.lamiral.info test1 ssl' ) ; + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'acc_compress_imap: test1.lamiral.info test1 ssl IsAuthenticated' ) ; + + + is( undef, acc_compress_imap( $acc ), "acc_compress_imap: test1.lamiral.info off ok" ) ; + is( undef, acc_compress_imap( $acc ), "acc_compress_imap: test1.lamiral.info 2nd off ok" ) ; + + note( 'Leaving tests_compress()' ) ; + return ; +} + + +sub acc_compress_imap +{ + my $acc = shift ; + + if ( ! defined( $acc ) ) { return ; } + + my $ret ; + my $imap = $acc->{ imap } ; + if ( ! defined $imap ) { return ; } + + if ( $imap && $acc->{ compress } ) + { + myprint( "$acc->{ Side }: Trying to turn imap compression on. Use --nocompress" . $acc->{ N } . " to avoid compression on " . lc( $acc->{ Side } ) . "\n" ) ; + if ( $ret = $imap->compress() ) + { + myprint( "$acc->{ Side }: Compression is on now\n" ) ; + } + else + { + myprint( "$acc->{ Side }: Failed to turn compression on\n" ) ; + } + } + else + { + myprint( "$acc->{ Side }: Compression is off. Use --compress" . $acc->{ N } . " to allow compression on " . lc( $acc->{ Side } ) . "\n" ) ; + } + # $ret is $acc->{ imap } on success, undef on failure or when there is nothing to do. + return $ret ; +} + +sub tests_login_imap +{ + note( 'Entering tests_login_imap()' ) ; + + is( undef, login_imap( ), 'login_imap: no args => undef' ) ; + + SKIP: { + if ( skip_macosx( ) ) + { + skip( 'Tests avoided only on binary on host polarhome macosx, no clue "ssl3_get_server_certificate:certificate verify failed"', 15 ) ; + } + else{ + + my $myimap ; + my $acc = {} ; + $acc->{ Side } = 'HostK' ; + $acc->{ authmech } = 'LOGIN' ; + #$IO::Socket::SSL::DEBUG = 4 ; + # Each month (trimester?): + # echo | openssl s_client -crlf -connect test1.lamiral.info:993 + # ... + # certificate has expired + # Fix: ssh root@test1.lamiral.info 'apt update && apt upgrade && /etc/init.d/dovecot restart' + # + # or + # echo | openssl s_client -crlf -connect test1.lamiral.info:993 + # ... + # Verify return code: 9 (certificate is not yet valid) + # Fix: /etc/init.d/openntpd restart + # 2021_09_04 done + ok( + $myimap = login_imap( 'test1.lamiral.info', 993, 'test1', 'secret1', + 1, undef, + 1, 100, $acc, {}, + ), 'login_imap: test1.lamiral.info test1 ssl' ) ; + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'login_imap: test1.lamiral.info test1 ssl IsAuthenticated' ) ; + + is( $myimap, $acc->{ imap }, "login_imap: acc->{ imap } ok test1 ssl") ; + + ok( + $myimap = login_imap( 'test1.lamiral.info', 143, 'test1', 'secret1', + 0, undef, + 1, 100, $acc, {}, + ), 'login_imap: test1.lamiral.info test1 tls' ) ; + ok( $myimap && $myimap->IsAuthenticated( ), 'login_imap: test1.lamiral.info test1 tls IsAuthenticated' ) ; + is( $myimap, $acc->{ imap }, "login_imap: acc->{ imap } ok test1 tls") ; + + #$IO::Socket::SSL::DEBUG = 4 ; + $acc->{sslargs} = { SSL_version => 'SSLv2' } ; + # SSLv2 not supported + is( + undef, $myimap = login_imap( 'test1.lamiral.info', 143, 'test1', 'secret1', + 0, undef, + 1, 100, $acc, {}, + ), 'login_imap: test1.lamiral.info test1 tls SSLv2 not supported' ) ; +#SSL_verify_mode => 1 +#SSL_version => 'TLSv1_1' + is( undef, $acc->{ imap }, "login_imap: acc->{ imap } test1 tls error => undef") ; + + + # I have left ? exit_clean to be replaced by errors_incr( $mysync, 'error message' ) + # 1 in login_imap() + + + my $mysync = {} ; + $acc = {} ; + $acc->{ Side } = 'Host2' ; + $acc->{ authmech } = 'LOGIN' ; + is( + undef, login_imap( 'noresol.lamiral.info', 143, 'test1', 'secret1', + 0, undef, + 1, 100, $acc, $mysync, + ), 'login_imap: noresol.lamiral.info undef' ) ; + + is( 'ERR_CONNECTION_FAILURE_HOST2', errorsanalyse( errors_log( $mysync ) ), 'login_imap: Host2 noresol.lamiral.info => ERR_CONNECTION_FAILURE_HOST2' ) ; + is( undef, $acc->{ imap }, "login_imap: acc->{ imap } noresol error => undef") ; + + # authentication failure for user2 + $mysync = {} ; + is( + undef, login_imap( 'test1.lamiral.info', 143, 'test1', 'Ce crétin', + 0, undef, + 1, 100, $acc, $mysync, + ), 'login_imap: user2 bad passord => undef' ) ; + + is( 'ERR_AUTHENTICATION_FAILURE_USER2', errorsanalyse( errors_log( $mysync ) ), 'login_imap: Host2 bad password => ERR_AUTHENTICATION_FAILURE_USER2' ) ; + + # authentication failure for user1 + $mysync = {} ; + $acc = {} ; + $acc->{ Side } = 'Host1' ; + $acc->{ authmech } = 'LOGIN' ; + is( + undef, login_imap( 'test1.lamiral.info', 143, 'test1', 'Ce crétin', + 0, undef, + 1, 100, $acc, $mysync, + ), 'login_imap: user1 bad passord => undef' ) ; + + is( 'ERR_AUTHENTICATION_FAILURE_USER1', errorsanalyse( errors_log( $mysync ) ), 'login_imap: Host1 bad password => ERR_AUTHENTICATION_FAILURE_USER1' ) ; + + } + } + + note( 'Leaving tests_login_imap()' ) ; + return ; +} + +sub oauthgenerateaccess +{ + if ( "petite" eq hostname() ) + { + myprint( "oauthgenerateaccess\n" ) ; + my @output = backtick( 'cd oauth2 && pwd && ./generate_gmail_token imapsync.gl0@gmail.com' ) ; + myprint( @output ) ; + } + return ; +} + +sub tests_login_imap_oauth +{ + note( 'Entering tests_login_imap_oauth()' ) ; + + oauthgenerateaccess() ; + + SKIP: { + if ( skip_macosx_binary( ) ) + { + skip( 'Tests avoided only on binary on host polarhome macosx, no clue "ssl3_get_server_certificate:certificate verify failed"', 6 ) ; + } + else + { + + my $mysync ; + my $acc ; + # oauthdirect authentication failure for user2 + $mysync = {} ; + $acc = {} ; + $acc->{ oauthdirect } = 'caca2' ; + $acc->{ debugimap } = 1 ; + $mysync->{ showpasswords } = 1 ; + $acc->{ Side } = 'Host2' ; + $acc->{ authmech } = 'QQQ' ; + is( + undef, login_imap( 'imap.gmail.com', 993, 'test1', 'Ce crétin', + 1, undef, + 1, 100, $acc, $mysync, + ), 'login_imap: user2 bad oauthdirect => undef' ) ; + + is( 'ERR_AUTHENTICATION_FAILURE_USER2', errorsanalyse( errors_log( $mysync ) ), 'login_imap: Host2 bad oauthdirect => ERR_AUTHENTICATION_FAILURE_USER2' ) ; + + # oauthdirect authentication failure for user1 + $mysync = {} ; + $acc = {} ; + $acc->{ Side } = 'Host1' ; + $acc->{ oauthdirect } = 'caca1' ; + $acc->{ debugimap } = 1 ; + $mysync->{ showpasswords } = 1 ; + $acc->{ authmech } = 'QQQ' ; + is( + undef, login_imap( 'imap.gmail.com', 993, 'test1', 'Ce crétin', + 1, undef, + 1, 100, $acc, $mysync, + ), 'login_imap: user1 bad oauthdirect => undef' ) ; + + is( 'ERR_AUTHENTICATION_FAILURE_USER1', errorsanalyse( errors_log( $mysync ) ), 'login_imap: Host1 bad oauthdirect => ERR_AUTHENTICATION_FAILURE_USER1' ) ; + + # oauthdirect authentication failure for user1 + $mysync = {} ; + $acc = {} ; + $acc->{ Side } = 'Host1' ; + $acc->{ oauthdirect } = '' ; + $acc->{ debugimap } = 1 ; + $mysync->{ showpasswords } = 1 ; + $acc->{ authmech } = 'QQQ' ; + is( + undef, login_imap( 'imap.gmail.com', 993, 'test1', 'Ce crétin', + 1, undef, + 1, 100, $acc, $mysync, + ), 'login_imap: user1 bad oauthdirect => undef' ) ; + + is( 'ERR_AUTHENTICATION_FAILURE_USER1', errorsanalyse( errors_log( $mysync ) ), 'login_imap: Host1 no oauthdirect value => ERR_AUTHENTICATION_FAILURE_USER1' ) ; + + } + } + + # oauthdirect authentication success for user1 + SKIP: { + if ( ! -r 'oauth2/D_oauth2_oauthdirect_imapsync.gl0@gmail.com.txt' ) + { + skip( 'oauthdirect: no oauthdirect file', 6 ) ; + } + my $myimap ; + my $mysync = {} ; + my $acc = {} ; + $acc->{ Side } = 'Host1' ; + $acc->{ oauthdirect } = 'oauth2/D_oauth2_oauthdirect_imapsync.gl0@gmail.com.txt' ; + $acc->{ debugimap } = 1 ; + $mysync->{ showpasswords } = 1 ; + $acc->{ authmech } = 'QQQ' ; + isa_ok( + $myimap = login_imap( 'imap.gmail.com', 993, 'user_useless', 'password_useless', + 1, undef, + 1, 100, $acc, $mysync, + ), 'Mail::IMAPClient', 'login_imap: user1 good oauthdirect => Mail::IMAPClient' ) ; + + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'login_imap: gmail oauth2 oauthdirect IsAuthenticated' ) ; + + ok( defined( $myimap ) && $myimap->logout( ), 'login_imap: gmail oauth2 oauthdirect logout' ) ; + ok( defined( $myimap ) && ! $myimap->IsAuthenticated( ), 'login_imap: gmail oauth2 oauthdirect not IsAuthenticated after logout' ) ; + ok( defined( $myimap ) && $myimap->reconnect( ), 'login_imap: gmail oauth2 oauthdirect reconnect ok' ) ; + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'login_imap: gmail oauth2 oauthdirect IsAuthenticated after reconnect' ) ; + } + + + + # oauthaccesstoken authentication success for user1 + SKIP: { + if ( ! -r 'oauth2/D_oauth2_access_token_imapsync.gl0@gmail.com.txt' ) + { + skip( 'oauthaccesstoken: no access_token file', 6 ) ; + } + my $myimap ; + my $mysync = {} ; + my $acc = {} ; + $acc->{ Side } = 'Host1' ; + $acc->{ oauthaccesstoken } = 'oauth2/D_oauth2_access_token_imapsync.gl0@gmail.com.txt' ; + $acc->{ debugimap } = 1 ; + $mysync->{ showpasswords } = 1 ; + $acc->{ authmech } = 'QQQ' ; + isa_ok( + $myimap = login_imap( 'imap.gmail.com', 993, 'imapsync.gl0@gmail.com', 'password_useless', + 1, undef, + 1, 100, $acc, $mysync, + ), 'Mail::IMAPClient', 'login_imap: user1 good oauthaccesstoken => Mail::IMAPClient' ) ; + + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'login_imap: gmail oauth2 oauthaccesstoken IsAuthenticated' ) ; + + ok( defined( $myimap ) && $myimap->logout( ), 'login_imap: gmail oauth2 oauthaccesstoken logout' ) ; + ok( defined( $myimap ) && ! $myimap->IsAuthenticated( ), 'login_imap: gmail oauth2 oauthaccesstoken not IsAuthenticated after logout' ) ; + ok( defined( $myimap ) && $myimap->reconnect( ), 'login_imap: gmail oauth2 oauthaccesstoken reconnect ok' ) ; + ok( defined( $myimap ) && $myimap->IsAuthenticated( ), 'login_imap: gmail oauth2 oauthaccesstoken IsAuthenticated after reconnect' ) ; + } + + + note( 'Leaving tests_login_imap_oauth()' ) ; + return ; +} + + + +sub login_imap +{ + my @allargs = @_ ; + my( + $host, $port, $user, $password, + $ssl, $tls, + $uid, $split, $acc, $mysync ) = @allargs ; + + $acc->{ imap } = undef ; + + if ( ! all_defined( $host, $port, $user, $acc->{ Side } ) ) + { + return ; + } + + my $side = lc $acc->{ Side } ; + myprint( "$acc->{ Side }: connecting and login on $side [$host] port [$port] with user [$user]\n" ) ; + + my $imap = init_imap( @allargs ) ; + + if ( ! $imap->connect() ) + { + my $error = "$acc->{ Side } failure: can not open imap connection on $side [$host] with user [$user]: " + . $imap->LastError . " $OS_ERROR\n" ; + errors_incr( $mysync, $error ) ; + return ; + } + myprint( "$acc->{ Side } IP address: ", $imap->Socket->peerhost(), "\n" ) ; + my $banner = $imap->Results()->[0] ; + + myprint( "$acc->{ Side } banner: $banner" ) ; + myprint( "$acc->{ Side } capability before authentication: ", join(q{ }, @{ $imap->capability() || [] }), "\n" ) ; + + if ( (! $ssl) and (! defined $tls ) and $imap->has_capability( 'STARTTLS' ) ) { + myprint( "$acc->{ Side }: going to ssl because STARTTLS is in CAPABILITY. Use --notls1 or --notls2 to avoid that behavior\n" ) ; + $tls = 1 ; + } + + + #myprint( Data::Dumper->Dump( [ @allargs ] ) ) ; + if ( $tls ) { + set_tls( $imap, $acc ) ; + + if ( ! $imap->starttls( ) ) + { + my $error = "$acc->{ Side } failure: Can not go to tls encryption on $side [$host]: " + . $imap->LastError . "\n" ; + + errors_incr( $mysync, $error ) ; + return ; + } + myprint( "$acc->{ Side }: Socket successfully converted to SSL\n" ) ; + } + + if ( $acc->{ authmech } eq 'PREAUTH' ) { + if ( $imap->IsAuthenticated( ) ) { + $imap->Socket ; + myprintf("%s: Assuming PREAUTH for %s\n", $acc->{ Side }, $imap->Server ) ; + }else{ + $mysync->{nb_errors}++ ; + exit_clean( + $mysync, $EXIT_AUTHENTICATION_FAILURE, + "$acc->{ Side } failure: error login on $side [$host] with user [$user] auth [PREAUTH]\n" + ) ; + } + } + + + + if ( authenticate_imap( $imap, @allargs ) ) + { + myprint( "$acc->{ Side }: success login on [$host] with user [$user] auth [$acc->{ authmech }] or [LOGIN]\n" ) ; + $acc->{ imap } = $imap ; + return( $imap ) ; + } + else + { + # The errors are already printed + myprint( "$acc->{ Side }: failed login on [$host] with user [$user] auth [$acc->{ authmech }]\n" ) ; + return ; + } +} + + + +sub init_imap +{ + my( + $host, $port, $user, $password, + $ssl, $tls, + $uid, $split, $acc, $mysync ) = @_ ; + + my ( $imap ) ; + + $imap = Mail::IMAPClient->new() ; + + if ( $mysync->{ tee } ) + { + # Well, it does not change anything, does it? + # It does when suppressing the hack with *STDERR + $imap->Debug_fh( $mysync->{ tee } ) ; + } + + if ( $ssl ) { set_ssl( $imap, $acc ) } + if ( $tls ) { } # can not do set_tls() here because connect() will directly do a STARTTLS + $imap->Clear( 1 ) ; + $imap->Server( $host ) ; + $imap->Port( $port ) ; + $imap->Fast_io( $acc->{ fastio } ) ; + $imap->Buffer( $buffersize || $DEFAULT_BUFFER_SIZE ) ; + $imap->Uid( $uid ) ; + + $imap->Peek( 1 ) ; + $imap->Debug( $acc->{ debugimap } ) ; + if ( $mysync->{ showpasswords } ) { + $imap->Showcredentials( 1 ) ; + } + + if ( defined( $acc->{ timeout } ) ) + { + $imap->Timeout( $acc->{ timeout } ) ; + } + + if ( defined $acc->{ keepalive } ) + { + $imap->Keepalive( $acc->{ keepalive } ) ; + } + + if ( defined $acc->{ reconnectretry } ) + { + $imap->Reconnectretry( $acc->{ reconnectretry } ) ; + } + + $imap->{IMAPSYNC_RECONNECT_COUNT} = 0 ; + $imap->Ignoresizeerrors( $allowsizemismatch ) ; + $split and $imap->Maxcommandlength( $SPLIT_FACTOR * $split ) ; + + + return( $imap ) ; + +} + +sub authenticate_imap +{ + my( $imap, + $host, $port, $user, $password, + $ssl, $tls, + $uid, $split, $acc, $mysync ) = @_ ; + + check_capability( $imap, $acc->{ authmech }, $acc->{ Side } ) ; + $imap->User( $user ) ; + + if ( defined $acc->{ domain } ) + { + $imap->Domain( $acc->{ domain } ) ; + $mysync->{ debug } and myprint( "Domain: $acc->{ domain }\n" ) ; + } + + $imap->Authuser( $acc->{ authuser } ) ; + $imap->Password( $password ) ; + + if ( 'X-MASTERAUTH' eq $acc->{ authmech } ) + { + xmasterauth( $imap ) ; + return 1 ; + } + + + if ( defined $acc->{ oauthdirect } ) + { + $acc->{ authmech } = 'XOAUTH2 direct' ; + return( oauthdirect( $mysync, $acc, $imap, $host, $user ) ) ; + } + + + if ( defined $acc->{ oauthaccesstoken } ) + { + $acc->{ authmech } = 'XOAUTH2 accesstoken' ; + return( oauthaccesstoken( $mysync, $acc, $imap, $host, $user ) ) ; + } + + + + + if ( $acc->{ proxyauth } ) { + $imap->Authmechanism(q{}) ; + $imap->User( $acc->{ authuser } ) ; + } else { + $imap->Authmechanism( $acc->{ authmech } ) unless ( $acc->{ authmech } eq 'LOGIN' or $acc->{ authmech } eq 'PREAUTH' ) ; + } + + $imap->Authcallback(\&xoauth2) if ( 'XOAUTH2' eq $acc->{ authmech } ) ; + $imap->Authcallback(\&plainauth) if ( ( 'PLAIN' eq $acc->{ authmech } ) or ( 'EXTERNAL' eq $acc->{ authmech } ) ) ; + + + unless ( $acc->{ authmech } eq 'PREAUTH' or $imap->login( ) ) { + my $info = "$acc->{ Side } failure: Error login on [$host] with user [$user] auth" ; + my $einfo = imap_last_error( $imap ) ; + my $error = "$info [$acc->{ authmech }]: $einfo\n" ; + + + if ( ( $acc->{ authmech } eq 'LOGIN' ) or $imap->IsUnconnected( ) or $acc->{ authuser } ) { + $acc->{ authuser } ||= "" ; + myprint( "$acc->{ Side } info: authmech [$acc->{ authmech }] user [$user] authuser [$acc->{ authuser }] IsUnconnected [", $imap->IsUnconnected( ), "]\n" ) ; + errors_incr( $mysync, $error ) ; + return ; + }else{ + errors_incr( $mysync, $error ) ; + } + + # It is not secure to try plain text LOGIN when another authmech failed + # but I do it anyway. This behavior is optional as option --notrylogin will skip it. + if ( $mysync->{ trylogin } ) + { + myprint( "$acc->{ Side } info: trying LOGIN Auth mechanism on [$host] with user [$user]. Use option --notrylogin to avoid this second chance to login via LOGIN auth\n" ) ; + $imap->Authmechanism(q{}) ; + if ( ! $imap->login( ) ) + { + failure_login( $mysync, $acc, 'LOGIN', $imap, $host, $user ) ; + return ; + } + else + { + myprint( "$acc->{ Side }: success login on [$host] with user [$user] auth [LOGIN] after [$acc->{ authmech }] failure\n" ) ; + } + } + else + { + myprint( "$acc->{ Side } info: not trying LOGIN Auth mechanism on [$host] with user [$user]. Use option --trylogin to have this second chance to login via LOGIN auth\n" ) ; + return ; + } + } + + if ( $acc->{ proxyauth } ) { + if ( ! $imap->proxyauth( $user ) ) { + failure_proxyauth( $mysync, $acc, $acc->{ authmech }, $imap, $host, $user ) ; + return ; + } + } + + return 1; +} + + +sub failure_login +{ + my( $mysync, $acc, $authmech, $imap, $host, $user ) = @ARG ; + my $info = "$acc->{ Side } failure: Error login on [$host] with user [$user] auth" ; + my $einfo = imap_last_error( $imap ) ; + my $error = "$info [$authmech]: $einfo\n" ; + errors_incr( $mysync, $error ) ; + return ; +} + +# failure_login and failure_proxyauth function are similar but +# variable $error so no factoring +sub failure_proxyauth +{ + my( $mysync, $acc, $authmech, $imap, $host, $user ) = @ARG ; + my $info = "$acc->{ Side } failure: Error login on [$host] with user [$user] auth" ; + my $einfo = imap_last_error( $imap ) ; + my $error = "$info [$authmech] using proxy-login as [$acc->{ authuser }]: $einfo\n" ; + errors_incr( $mysync, $error ) ; + return ; +} + + + + +sub oauthdirect +{ + my( $mysync, $acc, $imap, $host, $user ) = @_ ; + + my $oauthdirect_str ; + if ( -f -r $acc->{ oauthdirect } ) + { + $oauthdirect_str = firstline( $acc->{ oauthdirect } ) ; + } + else + { + $oauthdirect_str = $acc->{ oauthdirect } || 'Please define oauthdirect value' ; + } + + $imap->Authmechanism( 'XOAUTH2' ) ; + $imap->Authcallback( sub { return $oauthdirect_str } ) ; + + #if ( $imap->authenticate('XOAUTH2', sub { return $oauthdirect_str } ) ) + if ( $imap->login( ) ) + { + return 1 ; + } + else + { + failure_login( $mysync, $acc, $acc->{ authmech }, $imap, $host, $user ) ; + return ; + } + return ; +} + + + + +sub oauthaccesstoken +{ + my( $mysync, $acc, $imap, $host, $user ) = @_ ; + + my $oauthaccesstoken_str ; + if ( -f -r $acc->{ oauthaccesstoken } ) + { + $oauthaccesstoken_str = firstline( $acc->{ oauthaccesstoken } ) ; + } + else + { + $oauthaccesstoken_str = $acc->{ oauthaccesstoken } || 'Please define oauthaccesstoken value' ; + } + + my $oauth_string = "user=" . $user . "\x01auth=Bearer ". $oauthaccesstoken_str . "\x01\x01" ; + #myprint "oauth_string: $oauth_string\n" ; + + my $oauth_string_base64 = encode_base64( $oauth_string , '' ) ; + #myprint "oauth_string_base64: $oauth_string_base64\n" ; + + my $oauthdirect_str = $oauth_string_base64 ; + + $imap->Authmechanism( 'XOAUTH2' ) ; + $imap->Authcallback( sub { return $oauthdirect_str } ) ; + + #if ( $imap->authenticate('XOAUTH2', sub { return $oauthdirect_str } ) ) + if ( $imap->login( ) ) + { + return 1 ; + } + else + { + failure_login( $mysync, $acc, $acc->{ authmech }, $imap, $host, $user ) ; + return ; + } + return ; +} + + + + +sub check_capability +{ + + my( $imap, $authmech, $Side ) = @_ ; + + + if ( $imap->has_capability( "AUTH=$authmech" ) + or $imap->has_capability( $authmech ) ) + { + myprintf("%s: %s says it has CAPABILITY for AUTHENTICATE %s\n", + $Side, $imap->Server, $authmech) ; + return ; + } + + if ( $authmech eq 'LOGIN' ) + { + # Well, the warning is so common and useless that I prefer to remove it + # No more "... says it has NO CAPABILITY for AUTHENTICATE LOGIN" + return ; + } + + + myprintf( "%s: %s says it has NO CAPABILITY for AUTHENTICATE %s\n", + $Side, $imap->Server, $authmech ) ; + + if ( $authmech eq 'PLAIN' ) + { + myprint( "$Side: frequently PLAIN is only supported with SSL, try --ssl or --tls options\n" ) ; + } + + return ; +} + +sub set_ssl +{ + my ( $imap, $acc ) = @_ ; + # SSL_version can be + # SSLv3 SSLv2 SSLv23 SSLv23:!SSLv2 (last one is the default in IO-Socket-SSL-1.953) + # + + my $sslargs_hash = $acc->{sslargs} ; + + my $sslargs_default = { + SSL_verify_mode => $SSL_VERIFY_POLICY, + SSL_verifycn_scheme => 'imap', + SSL_cipher_list => 'DEFAULT:!DH', + } ; + + # initiate with default values + my %sslargs_mix = %{ $sslargs_default } ; + # now override with passed values + @sslargs_mix{ keys %{ $sslargs_hash } } = values %{ $sslargs_hash } ; + # remove keys with undef values + foreach my $key ( keys %sslargs_mix ) { + delete $sslargs_mix{ $key } if ( not defined $sslargs_mix{ $key } ) ; + } + # back to an ARRAY + my @sslargs_mix = %sslargs_mix ; + #myprint( Data::Dumper->Dump( [ $sslargs_hash, $sslargs_default, \%sslargs_mix, \@sslargs_mix ] ) ) ; + $imap->Ssl( \@sslargs_mix ) ; + return ; +} + +sub set_tls +{ + my ( $imap, $acc ) = @_ ; + + my $sslargs_hash = $acc->{sslargs} ; + + my $sslargs_default = { + SSL_verify_mode => $SSL_VERIFY_POLICY, + SSL_cipher_list => 'DEFAULT:!DH', + } ; + #myprint( Data::Dumper->Dump( [ $acc, $sslargs_hash, $sslargs_default ] ) ) ; + + # initiate with default values + my %sslargs_mix = %{ $sslargs_default } ; + # now override with passed values + @sslargs_mix{ keys %{ $sslargs_hash } } = values %{ $sslargs_hash } ; + # remove keys with undef values + foreach my $key ( keys %sslargs_mix ) { + delete $sslargs_mix{ $key } if ( not defined $sslargs_mix{ $key } ) ; + } + # back to an ARRAY + my @sslargs_mix = %sslargs_mix ; + + $imap->Starttls( \@sslargs_mix ) ; + return ; +} + + + +sub plainauth +{ + my $code = shift; + my $imap = shift; + + my $string = mysprintf("%s\x00%s\x00%s", $imap->User, + defined $imap->Authuser ? $imap->Authuser : "", $imap->Password); + return encode_base64("$string", q{}); +} + +# Copy from https://github.com/imapsync/imapsync/pull/25/files +# Changes "use" pragmas to "require". +# The openssl system call shall be replaced by pure Perl and +# https://metacpan.org/pod/Crypt::OpenSSL::PKCS12 + +# Now the Joaquin Lopez code: +# +# Used this as an example: https://gist.github.com/gsainio/6322375 +# +# And this as a reference: https://developers.google.com/accounts/docs/OAuth2ServiceAccount +# (note there is an http/rest tab, where the real info is hidden away... went on a witch hunt +# until I noticed that...) +# +# This is targeted at gmail to maintain compatibility after google's oauth1 service is deactivated +# on May 5th, 2015: https://developers.google.com/gmail/oauth_protocol +# If there are other oauth2 implementations out there, this would need to be modified to be +# compatible +# +# This is a good guide on setting up the google api/apps side of the equation: +# http://www.limilabs.com/blog/oauth2-gmail-imap-service-account +# +# 2016/05/27: Updated to support oauth/key data in the .json files Google now defaults to +# when creating gmail service accounts. They're easier to work with since they neither +# requiring decrypting nor specifying the oauth2 client id separately. +# +# If the password arg ends in .json, it will assume this new json method, otherwise it +# will fallback to the "oauth client id;.p12" format it was previously using. +sub xoauth2 +{ + require JSON::WebToken ; + require LWP::UserAgent ; + require HTML::Entities ; + require JSON ; + require JSON::WebToken::Crypt::RSA ; + require Crypt::OpenSSL::PKCS12; + require Crypt::OpenSSL::RSA ; + require Encode::Byte ; + require IO::Socket::SSL ; + + my $code = shift; + my $imap = shift; + + my ($iss,$key); + + if( $imap->Password =~ /^(.*\.json)$/x ) + { + my $json = JSON->new( ) ; + my $filename = $1; + $sync->{ debug } and myprint( "XOAUTH2 json file: $filename\n" ) ; + my $FILE ; + if ( ! open( $FILE, '<', $filename ) ) + { + $sync->{nb_errors}++ ; + exit_clean( $sync, $EXIT_AUTHENTICATION_FAILURE, + "error [$filename]: $OS_ERROR\n" + ) ; + } + my $jsonfile = $json->decode( join q{}, <$FILE> ) ; + close $FILE ; + + $iss = $jsonfile->{client_id}; + $key = $jsonfile->{private_key}; + $sync->{ debug } and myprint( "Service account: $iss\n"); + $sync->{ debug } and myprint( "Private key:\n$key\n"); + } + else + { + # Get iss (service account address), keyfile name, and keypassword if necessary + ( $iss, my $keyfile, my $keypass ) = $imap->Password =~ /([\-\d\w\@\.]+);([a-zA-Z0-9 \_\-\.\/]+);?(.*)?/x ; + + # Assume key password is google default if not provided + $keypass = 'notasecret' if not $keypass; + + $sync->{ debug } and myprint( "Service account: $iss\nKey file: $keyfile\nKey password: $keypass\n"); + + # Get private key from p12 file + my $pkcs12 = Crypt::OpenSSL::PKCS12->new_from_file($keyfile); + $key = $pkcs12->private_key($keypass); + + $sync->{ debug } and myprint( "Private key:\n$key\n"); + } + + # Create jwt of oauth2 request + my $time = time ; + my $jwt = JSON::WebToken->encode( { + 'iss' => $iss, # service account + 'scope' => 'https://mail.google.com/', + 'aud' => 'https://www.googleapis.com/oauth2/v3/token', + 'exp' => $time + $DEFAULT_EXPIRATION_TIME_OAUTH2_PK12, + 'iat' => $time, + 'prn' => $imap->User # user to auth as + }, + $key, 'RS256', {'typ' => 'JWT'} ); # Crypt::OpenSSL::RSA needed here. + + # Post oauth2 request + my $ua = LWP::UserAgent->new( ) ; + $ua->env_proxy( ) ; + + my $response = $ua->post('https://www.googleapis.com/oauth2/v3/token', + { grant_type => HTML::Entities::encode_entities('urn:ietf:params:oauth:grant-type:jwt-bearer'), + assertion => $jwt } ) ; + + unless( $response->is_success( ) ) { + $sync->{nb_errors}++ ; + exit_clean( $sync, $EXIT_AUTHENTICATION_FAILURE, + $response->code, "\n", $response->content, "\n" + ) ; + }else{ + $sync->{ debug } and myprint( $response->content ) ; + } + + # access_token in response is what we need + my $data = JSON::decode_json( $response->content ) ; + + # format as oauth2 auth data + my $xoauth2_string = encode_base64( 'user=' . $imap->User . "\1auth=Bearer " . $data->{access_token} . "\1\1", q{} ) ; + + $sync->{ debug } and myprint( "XOAUTH2 String: $xoauth2_string\n"); + return($xoauth2_string); +} + + + + +sub xmasterauth +{ + # This is Kerio auth admin + # This code comes from + # https://github.com/imapsync/imapsync/pull/53/files + + my $imap = shift ; + + my $user = $imap->User( ) ; + my $password = $imap->Password( ) ; + my $authmech = 'X-MASTERAUTH' ; + + my @challenge = $imap->tag_and_run( $authmech, "+" ) ; + if ( not defined $challenge[0] ) + { + $sync->{nb_errors}++ ; + exit_clean( $sync, $EXIT_AUTHENTICATION_FAILURE, + "Failure authenticate with $authmech: ", + $imap->LastError, "\n" + ) ; + return ; # hahaha! + } + $sync->{ debug } and myprint( "X-MASTERAUTH challenge: [@challenge]\n" ) ; + + $challenge[1] =~ s/^\+ |^\s+|\s+$//g ; + if ( ! $imap->_imap_command( { addcrlf => 1, addtag => 0, tag => $imap->Count }, md5_hex( $challenge[1] . $password ) ) ) + { + $sync->{nb_errors}++ ; + exit_clean( $sync, $EXIT_AUTHENTICATION_FAILURE, + "Failure authenticate with $authmech: ", + $imap->LastError, "\n" + ) ; + } + + if ( ! $imap->tag_and_run( 'X-SETUSER ' . $user ) ) + { + $sync->{nb_errors}++ ; + exit_clean( $sync, $EXIT_AUTHENTICATION_FAILURE, + "Failure authenticate with $authmech: ", + "X-SETUSER ", $imap->LastError, "\n" + ) ; + } + + $imap->State( Mail::IMAPClient::Authenticated ) ; + # I comment this state because "Selected" state is usually done by SELECT or EXAMINE imap commands + # $imap->State( Mail::IMAPClient::Selected ) ; + + return ; +} + +sub keepalive1 +{ + my $mysync = shift ; + + $mysync->{ acc1 }->{ keepalive } = defined $mysync->{ acc1 }->{ keepalive } ? $mysync->{ acc1 }->{ keepalive } : 1 ; + + if ( $mysync->{ acc1 }->{ keepalive } ) + { + myprint( "Host1: imap connection keepalive is on on host1. Use --nokeepalive1 to disable it.\n" ) ; + } + else + { + myprint( "Host1: imap connection keepalive is off on host1. Use --keepalive1 to enable it.\n" ) ; + } +} + +sub keepalive2 +{ + my $mysync = shift ; + + $mysync->{ acc2 }->{ keepalive } = defined $mysync->{ acc2 }->{ keepalive } ? $mysync->{ acc2 }->{ keepalive } : 1 ; + + if ( $mysync->{ acc2 }->{ keepalive } ) + { + myprint( "Host2: imap connection keepalive is on on host2. Use --nokeepalive2 to disable it.\n" ) ; + } + else + { + myprint( "Host2: imap connection keepalive is off on host2. Use --keepalive2 to enable it.\n" ) ; + } +} + + + +sub banner_imapsync +{ + my $mysync = shift @ARG ; + my @argv = @ARG ; + + my $banner_imapsync = join q{}, + q{$RCSfile: imapsync,v $ }, + q{$Revision: 2.178 $ }, + q{$Date: 2022/01/12 21:28:37 $ }, + "\n", + "Command line used, run by $EXECUTABLE_NAME:\n", + "$PROGRAM_NAME ", command_line_nopassword( $mysync, @argv ), "\n" ; + + return( $banner_imapsync ) ; +} + +sub tests_do_valid_directory +{ + note( 'Entering tests_do_valid_directory()' ) ; + + is( 1, do_valid_directory( '.'), 'do_valid_directory: . good' ) ; + is( 1, do_valid_directory( './W/tmp/tests/valid/sub'), 'do_valid_directory: ./W/tmp/tests/valid/sub good' ) ; + + Readonly my $NB_UNIX_tests_do_valid_directory_non_root => 2 ; + diag( "OSNAME=$OSNAME EFFECTIVE_USER_ID=$EFFECTIVE_USER_ID" ) ; + + SKIP: { + skip( 'Tests only for non roor user', $NB_UNIX_tests_do_valid_directory_non_root ) if ( '0' eq $EFFECTIVE_USER_ID ) ; + diag( 'The "Error / is not writable" is on purpose' ) ; + ok( 0 == do_valid_directory( '/'), 'do_valid_directory: / bad' ) ; + diag( 'The "Error permission denied" on /noway is on purpose' ) ; + ok( 0 == do_valid_directory( '/noway'), 'do_valid_directory: /noway bad' ) ; + } + + + note( 'Leaving tests_do_valid_directory()' ) ; + return ; +} + +sub do_valid_directory +{ + my $dir = shift @ARG ; + + # all good => return ok. + return( 1 ) if ( -d $dir and -r _ and -w _ ) ; + + # exist but bad + if ( -e $dir and not -d _ ) { + myprint( "Error: $dir exists but is not a directory\n" ) ; + return( 0 ) ; + } + if ( -e $dir and not -w _ ) { + my $sb = stat $dir ; + myprintf( "Error: directory %s is not writable for user %s, permissions are %04o and owner is %s ( uid %s )\n", + $dir, getpwuid_any_os( $EFFECTIVE_USER_ID ), ($sb->mode & oct($PERMISSION_FILTER) ), getpwuid_any_os( $sb->uid ), $sb->uid( ) ) ; + return( 0 ) ; + } + # Trying to create it + myprint( "Creating directory $dir (current directory is " . getcwd( ) . ")\n" ) ; + if ( ! eval { mkpath( $dir ) } ) { + myprint( "$EVAL_ERROR" ) if ( $EVAL_ERROR ) ; + } + return( 1 ) if ( -d $dir and -r _ and -w _ ) ; + return( 0 ) ; +} + + +sub tests_match_a_pid_number +{ + note( 'Entering tests_match_a_pid_number()' ) ; + + is( undef, match_a_pid_number( ), 'match_a_pid_number: no args => undef' ) ; + is( undef, match_a_pid_number( q{} ), 'match_a_pid_number: "" => undef' ) ; + is( undef, match_a_pid_number( 'lalala' ), 'match_a_pid_number: lalala => undef' ) ; + is( 1, match_a_pid_number( 1 ), 'match_a_pid_number: 1 => 1' ) ; + is( 1, match_a_pid_number( 123 ), 'match_a_pid_number: 123 => 1' ) ; + is( 1, match_a_pid_number( -123 ), 'match_a_pid_number: -123 => 1' ) ; + is( 1, match_a_pid_number( '123' ), 'match_a_pid_number: "123" => 1' ) ; + is( 1, match_a_pid_number( '-123' ), 'match_a_pid_number: "-123" => 1' ) ; + is( undef, match_a_pid_number( 'a123' ), 'match_a_pid_number: a123 => undef' ) ; + is( undef, match_a_pid_number( '-a123' ), 'match_a_pid_number: -a123 => undef' ) ; + is( 1, match_a_pid_number( 99999 ), 'match_a_pid_number: 99999 => 1' ) ; + is( 1, match_a_pid_number( -99999 ), 'match_a_pid_number: -99999 => 1' ) ; + is( undef, match_a_pid_number( 0 ), 'match_a_pid_number: 0 => undef' ) ; + is( 1, match_a_pid_number( 100000 ), 'match_a_pid_number: 100000 => 1' ) ; + is( 1, match_a_pid_number( 123456 ), 'match_a_pid_number: 123456 => 1' ) ; + is( undef, match_a_pid_number( '-0' ), 'match_a_pid_number: "-0" => undef' ) ; + is( 1, match_a_pid_number( -100000 ), 'match_a_pid_number: -100000 => 1' ) ; + is( 1, match_a_pid_number( -123456 ), 'match_a_pid_number: -123456 => 1' ) ; + is( 1, match_a_pid_number( 2**22 ), 'match_a_pid_number: 2**22 => 1' ) ; + is( undef, match_a_pid_number( 2**22 + 1 ), 'match_a_pid_number: 2**22 + 1 => undef' ) ; + is( undef, match_a_pid_number( 4194304 + 1 ), 'match_a_pid_number: 2**22 + 1 = 4194305 => undef' ) ; + + note( 'Leaving tests_match_a_pid_number()' ) ; + return ; +} + +sub match_a_pid_number +{ + my $pid = shift @ARG ; + if ( ! defined $pid ) { return ; } + #print "$pid\n" ; + if ( ! match( $pid, '^-?\d+$' ) ) { return ; } + #print "$pid\n" ; + # can be negative on Windows + #if ( 0 > $pid ) { return ; } + #if ( 65535 < $pid ) { return ; } + if ( 2**22 < abs( $pid ) ) { return ; } + if ( 0 == abs( $pid ) ) { return ; } + return 1 ; +} + +sub tests_remove_pidfile_not_running +{ + note( 'Entering tests_remove_pidfile_not_running()' ) ; + + ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'remove_pidfile_not_running: mkpath W/tmp/tests/' ) ; + is( undef, remove_pidfile_not_running( ), 'remove_pidfile_not_running: no args => undef' ) ; + is( undef, remove_pidfile_not_running( './W' ), 'remove_pidfile_not_running: a dir => undef' ) ; + is( undef, remove_pidfile_not_running( 'noexists' ), 'remove_pidfile_not_running: noexists => undef' ) ; + is( 1, touch( 'W/tmp/tests/empty.pid' ), 'remove_pidfile_not_running: prepa empty W/tmp/tests/empty.pid' ) ; + is( undef, remove_pidfile_not_running( 'W/tmp/tests/empty.pid' ), 'remove_pidfile_not_running: W/tmp/tests/empty.pid => undef' ) ; + is( 'lalala', string_to_file( 'lalala', 'W/tmp/tests/lalala.pid' ), 'remove_pidfile_not_running: prepa W/tmp/tests/lalala.pid' ) ; + is( undef, remove_pidfile_not_running( 'W/tmp/tests/lalala.pid' ), 'remove_pidfile_not_running: W/tmp/tests/lalala.pid => undef' ) ; + is( '55555', string_to_file( '55555', 'W/tmp/tests/notrunning.pid' ), 'remove_pidfile_not_running: prepa W/tmp/tests/notrunning.pid' ) ; + is( 1, remove_pidfile_not_running( 'W/tmp/tests/notrunning.pid' ), 'remove_pidfile_not_running: W/tmp/tests/notrunning.pid => 1' ) ; + is( $PROCESS_ID, string_to_file( $PROCESS_ID, 'W/tmp/tests/running.pid' ), 'remove_pidfile_not_running: prepa W/tmp/tests/running.pid' ) ; + is( undef, remove_pidfile_not_running( 'W/tmp/tests/running.pid' ), 'remove_pidfile_not_running: W/tmp/tests/running.pid => undef' ) ; + + note( 'Leaving tests_remove_pidfile_not_running()' ) ; + return ; +} + +sub remove_pidfile_not_running +{ + # + my $pid_filename = shift @ARG ; + + #myprint( "In remove_pidfile_not_running $pid_filename\n" ) ; + if ( ! $pid_filename ) { myprint( "No variable pid_filename\n" ) ; return } ; + if ( ! -e $pid_filename ) + { + myprint( "File $pid_filename does not exist\n" ) ; + return ; + } + #myprint( "Still In remove_pidfile_not_running $pid_filename\n" ) ; + + if ( ! -f $pid_filename ) { myprint( "File $pid_filename is not a file\n" ) ; return } ; + + my $pid = firstline( $pid_filename ) ; + if ( ! match_a_pid_number( $pid ) ) { myprint( "In remove_pidfile_not_running: pid $pid in $pid_filename is not a pid number\n" ) ; return } ; + # can't kill myself => do nothing + if ( ! kill 'ZERO', $PROCESS_ID ) { myprint( "Can not kill ZERO myself $PROCESS_ID\n" ) ; return } ; + + # can't kill ZERO the pid => it is gone or own by another user => remove pidfile + if ( ! kill 'ZERO', $pid ) { + myprint( "Removing old $pid_filename since its PID $pid is not running anymore (oo-killed?)\n" ) ; + if ( unlink $pid_filename ) { + myprint( "Removed old $pid_filename\n" ) ; + return 1 ; + }else{ + myprint( "Could not remove old $pid_filename because $!\n" ) ; + return ; + } + } + myprint( "Another imapsync process $pid is running as says pidfile $pid_filename\n" ) ; + return ; +} + + +sub tests_tail +{ + note( 'Entering tests_tail()' ) ; + + ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'tail: mkpath W/tmp/tests/' ) ; + ok( ( ! -e 'W/tmp/tests/tail.pid' || unlink 'W/tmp/tests/tail.pid' ), 'tail: unlink W/tmp/tests/tail.pid' ) ; + ok( ( ! -e 'W/tmp/tests/tail.txt' || unlink 'W/tmp/tests/tail.txt' ), 'tail: unlink W/tmp/tests/tail.txt' ) ; + + is( undef, tail( ), 'tail: no args => undef' ) ; + my $mysync ; + is( undef, tail( $mysync ), 'tail: no pidfile => undef' ) ; + + $mysync->{pidfile} = 'W/tmp/tests/tail.pid' ; + is( undef, tail( $mysync ), 'tail: no pidfilelocking => undef' ) ; + + $mysync->{pidfilelocking} = 1 ; + is( undef, tail( $mysync ), 'tail: pidfile no exists => undef' ) ; + + + my $pidandlog = "33333\nW/tmp/tests/tail.txt\n" ; + is( $pidandlog, string_to_file( $pidandlog, $mysync->{pidfile} ), 'tail: put pid 33333 and tail.txt in pidfile' ) ; + is( undef, tail( $mysync ), 'tail: logfile to tail no exists => undef' ) ; + + my $tailcontent = "L1\nL2\nL3\nL4\nL5\n" ; + is( $tailcontent, string_to_file( $tailcontent, 'W/tmp/tests/tail.txt' ), + 'tail: put L1\nL2\nL3\nL4\nL5\n in W/tmp/tests/tail.txt' ) ; + + is( undef, tail( $mysync ), 'tail: fake pid in pidfile + tail off => 1' ) ; + + $mysync->{ tail } = 1 ; + is( 1, tail( $mysync ), 'tail: fake pid in pidfile + tail on=> 1' ) ; + + # put my own pid, won't do tail + $pidandlog = "$PROCESS_ID\nW/tmp/tests/tail.txt\n" ; + is( $pidandlog, string_to_file( $pidandlog, $mysync->{pidfile} ), 'tail: put my own PID in pidfile' ) ; + is( undef, tail( $mysync ), 'tail: my own pid in pidfile => undef' ) ; + + note( 'Leaving tests_tail()' ) ; + return ; +} + + + +sub tail +{ + # return undef on failures + # return 1 on success + + my $mysync = shift ; + + # no tail when aborting! + if ( $mysync->{ abort } ) { return ; } + + my $pidfile = $mysync->{pidfile} ; + my $lock = $mysync->{pidfilelocking} ; + my $tail = $mysync->{tail} ; + + if ( ! $pidfile ) { return ; } + if ( ! $lock ) { return ; } + if ( ! $tail ) { return ; } + + if ( ! -e $pidfile ) { return ; } + + my $pidtotail = firstline( $pidfile ) ; + if ( ! $pidtotail ) { return ; } + + + + # It should not happen but who knows... + if ( $pidtotail eq $PROCESS_ID ) { return ; } + + + my $filetotail = secondline( $pidfile ) ; + if ( ! $filetotail ) { return ; } + + if ( ! -r $filetotail ) + { + #myprint( "Error: can not read $filetotail\n" ) ; + return ; + } + + myprint( "Doing a tail -f on $filetotail for processus pid $pidtotail until it is finished.\n" ) ; + my $file = File::Tail->new( + name => $filetotail, + nowait => 1, + interval => 1, + tail => 1, + adjustafter => 2 + ); + + my $moretimes = 200 ; + # print one line at least + my $line = $file->read ; + myprint( $line ) ; + while ( isrunning( $pidtotail, \$moretimes ) and defined( $line = $file->read ) ) + { + myprint( $line ); + sleep( 0.02 ) ; + } + + return 1 ; +} + +sub isrunning +{ + my $pidtocheck = shift ; + my $moretimes_ref = shift ; + + if ( kill 'ZERO', $pidtocheck ) + { + #myprint( "$pidtocheck running\n" ) ; + return 1 ; + } + elsif ( $$moretimes_ref >= 0 ) + { + # continue to consider it running + $$moretimes_ref-- ; + return 1 ; + } + else + { + myprint( "Tailed processus $pidtocheck ended\n" ) ; + return ; + } +} + +sub tests_write_pidfile +{ + note( 'Entering tests_write_pidfile()' ) ; + + my $mysync ; + + is( 1, write_pidfile( ), 'write_pidfile: no args => 1' ) ; + + # no pidfile => ok + $mysync->{pidfile} = q{} ; + is( 1, write_pidfile( $mysync ), 'write_pidfile: no pidfile => undef' ) ; + + # The pidfile path is bad => failure + $mysync->{pidfile} = '/no/no/no.pid' ; + is( undef, write_pidfile( $mysync ), 'write_pidfile: no permission for /no/no/no.pid, no lock => undef' ) ; + + $mysync->{pidfilelocking} = 1 ; + is( undef, write_pidfile( $mysync ), 'write_pidfile: no permission for /no/no/no.pid + lock => undef' ) ; + + $mysync->{pidfile} = 'W/tmp/tests/test.pid' ; + ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'write_pidfile: mkpath W/tmp/tests/' ) ; + is( 1, touch( $mysync->{pidfile} ), 'write_pidfile: lock prepa' ) ; + + $mysync->{pidfilelocking} = 0 ; + is( 1, write_pidfile( $mysync ), 'write_pidfile: W/tmp/tests/test.pid + no lock => 1' ) ; + is( $PROCESS_ID, firstline( 'W/tmp/tests/test.pid' ), "write_pidfile: W/tmp/tests/test.pid contains $PROCESS_ID" ) ; + is( q{}, secondline( 'W/tmp/tests/test.pid' ), "write_pidfile: W/tmp/tests/test.pid contains no second line" ) ; + + $mysync->{pidfilelocking} = 1 ; + is( undef, write_pidfile( $mysync ), 'write_pidfile: W/tmp/tests/test.pid + lock => undef' ) ; + + + $mysync->{pidfilelocking} = 0 ; + $mysync->{ logfile } = 'rrrr.txt' ; + is( 1, write_pidfile( $mysync ), 'write_pidfile: W/tmp/tests/test.pid + no lock + logfile => 1' ) ; + is( $PROCESS_ID, firstline( 'W/tmp/tests/test.pid' ), "write_pidfile: + no lock + logfile W/tmp/tests/test.pid contains $PROCESS_ID" ) ; + is( q{rrrr.txt}, secondline( 'W/tmp/tests/test.pid' ), "write_pidfile: + no lock + logfile W/tmp/tests/test.pid contains rrrr.txt" ) ; + + + note( 'Leaving tests_write_pidfile()' ) ; + return ; +} + + + +sub write_pidfile +{ + # returns undef if something is considered fatal + # returns 1 otherwise + + #myprint( "In write_pidfile\n" ) ; + if ( ! @ARG ) { return 1 ; } + + my $mysync = shift @ARG ; + + # Do not write the pid file if the current process goal is to abort the process designed by the pid file + if ( $mysync->{ abort } ) { return 1 ; } + + # + my $pid_filename = $mysync->{ pidfile } ; + my $lock = $mysync->{ pidfilelocking } ; + + if ( ! $pid_filename ) + { + myprint( "PID file is unset ( to set it, use --pidfile filepath ; to avoid it use --pidfile \"\" )\n" ) ; + return( 1 ) ; + } + + myprint( "PID file is $pid_filename ( to change it, use --pidfile filepath ; to avoid it use --pidfile \"\" )\n" ) ; + if ( -e $pid_filename and $lock ) { + myprint( "$pid_filename already exists, another imapsync may be curently running. Aborting imapsync.\n" ) ; + return ; + + } + + if ( -e $pid_filename ) { + myprint( "$pid_filename already exists, overwriting it ( use --pidfilelocking to avoid concurrent runs )\n" ) ; + } + + my $pid_string = "$PROCESS_ID\n" ; + my $pid_message = "Writing my PID $PROCESS_ID in $pid_filename\n" ; + + if ( $mysync->{ logfile } ) + { + $pid_string .= "$mysync->{ logfile }\n" ; + $pid_message .= "Writing also my logfile name in $pid_filename : $mysync->{ logfile }\n" ; + } + + if ( open my $FILE_HANDLE, '>', $pid_filename ) { + myprint( $pid_message ) ; + print $FILE_HANDLE $pid_string ; + close $FILE_HANDLE ; + return( 1 ) ; + } + else + { + myprint( "Could not open $pid_filename for writing. Check permissions or disk space: $OS_ERROR\n" ) ; + return ; + } +} + + +sub fix_Inbox_INBOX_mapping +{ + my( $h1_all, $h2_all ) = @_ ; + + my $regex = q{} ; + SWITCH: { + if ( exists $h1_all->{INBOX} and exists $h2_all->{INBOX} ) { $regex = q{} ; last SWITCH ; } ; + if ( exists $h1_all->{Inbox} and exists $h2_all->{Inbox} ) { $regex = q{} ; last SWITCH ; } ; + if ( exists $h1_all->{INBOX} and exists $h2_all->{Inbox} ) { $regex = q{s/^INBOX$/Inbox/x} ; last SWITCH ; } ; + if ( exists $h1_all->{Inbox} and exists $h2_all->{INBOX} ) { $regex = q{s/^Inbox$/INBOX/x} ; last SWITCH ; } ; + } ; + return( $regex ) ; +} + +sub tests_fix_Inbox_INBOX_mapping +{ + note( 'Entering tests_fix_Inbox_INBOX_mapping()' ) ; + + + my( $h1_all, $h2_all ) ; + + $h1_all = { 'INBOX' => q{} } ; + $h2_all = { 'INBOX' => q{} } ; + ok( q{} eq fix_Inbox_INBOX_mapping( $h1_all, $h2_all ), 'fix_Inbox_INBOX_mapping: INBOX INBOX' ) ; + + $h1_all = { 'Inbox' => q{} } ; + $h2_all = { 'Inbox' => q{} } ; + ok( q{} eq fix_Inbox_INBOX_mapping( $h1_all, $h2_all ), 'fix_Inbox_INBOX_mapping: Inbox Inbox' ) ; + + $h1_all = { 'INBOX' => q{} } ; + $h2_all = { 'Inbox' => q{} } ; + ok( q{s/^INBOX$/Inbox/x} eq fix_Inbox_INBOX_mapping( $h1_all, $h2_all ), 'fix_Inbox_INBOX_mapping: INBOX Inbox' ) ; + + $h1_all = { 'Inbox' => q{} } ; + $h2_all = { 'INBOX' => q{} } ; + ok( q{s/^Inbox$/INBOX/x} eq fix_Inbox_INBOX_mapping( $h1_all, $h2_all ), 'fix_Inbox_INBOX_mapping: Inbox INBOX' ) ; + + $h1_all = { 'INBOX' => q{} } ; + $h2_all = { 'rrrrr' => q{} } ; + ok( q{} eq fix_Inbox_INBOX_mapping( $h1_all, $h2_all ), 'fix_Inbox_INBOX_mapping: INBOX rrrrrr' ) ; + + $h1_all = { 'rrrrr' => q{} } ; + $h2_all = { 'Inbox' => q{} } ; + ok( q{} eq fix_Inbox_INBOX_mapping( $h1_all, $h2_all ), 'fix_Inbox_INBOX_mapping: rrrrr Inbox' ) ; + + note( 'Leaving tests_fix_Inbox_INBOX_mapping()' ) ; + return ; +} + + +sub jux_utf8_list +{ + my @s_inp = @_ ; + my $s_out = q{} ; + foreach my $s ( @s_inp ) { + $s_out .= jux_utf8( $s ) . "\n" ; + } + return( $s_out ) ; +} + +sub tests_jux_utf8_list +{ + note( 'Entering tests_jux_utf8_list()' ) ; + + use utf8 ; + is( q{}, jux_utf8_list( ), 'jux_utf8_list: void' ) ; + is( "[]\n", jux_utf8_list( q{} ), 'jux_utf8_list: empty string' ) ; + is( "[INBOX]\n", jux_utf8_list( 'INBOX' ), 'jux_utf8_list: INBOX' ) ; + is( "[&ANY-] = [Ö]\n", jux_utf8_list( '&ANY-' ), 'jux_utf8_list: [&ANY-] = [Ö]' ) ; + + note( 'Leaving tests_jux_utf8_list()' ) ; + return( 0 ) ; +} + +# editing utf8 can be tricky without an utf8 editor +sub tests_jux_utf8_old +{ + note( 'Entering tests_jux_utf8_old()' ) ; + + no utf8 ; + + is( '[]', jux_utf8_old( q{} ), 'jux_utf8_old: void => []' ) ; + is( '[INBOX]', jux_utf8_old( 'INBOX'), 'jux_utf8_old: INBOX => [INBOX]' ) ; + is( '[&ZTZO9nux-] = [æâ€Â¶Ã¤Â»Â¶Ã§Â®Â±]', jux_utf8_old( '&ZTZO9nux-'), 'jux_utf8_old: => [&ZTZO9nux-] = [æâ€Â¶Ã¤Â»Â¶Ã§Â®Â±]' ) ; + is( '[&ANY-] = [Ö]', jux_utf8_old( '&ANY-'), 'jux_utf8_old: &ANY- => [&ANY-] = [Ö]' ) ; + # +BD8EQAQ1BDQEOwQ+BDM- SHOULD stay as is! + is( '[+BD8EQAQ1BDQEOwQ+BDM-] = [ÿрõôûþó]', jux_utf8_old( '+BD8EQAQ1BDQEOwQ+BDM-' ), 'jux_utf8_old: => [+BD8EQAQ1BDQEOwQ+BDM-] = [ÿрõôûþó]' ) ; + is( '[&BB8EQAQ+BDUEOgRC-] = [ßрþõúт]', jux_utf8_old( '&BB8EQAQ+BDUEOgRC-' ), 'jux_utf8_old: => [&BB8EQAQ+BDUEOgRC-] = [ßрþõúт]' ) ; + + note( 'Leaving tests_jux_utf8_old()' ) ; + return ; +} + +sub jux_utf8_old +{ + # juxtapose utf8 at the right if different + my ( $s_utf7 ) = shift ; + my ( $s_utf8 ) = imap_utf7_decode_old( $s_utf7 ) ; + + if ( $s_utf7 eq $s_utf8 ) { + #myprint( "[$s_utf7]\n" ) ; + return( "[$s_utf7]" ) ; + }else{ + #myprint( "[$s_utf7] = [$s_utf8]\n" ) ; + return( "[$s_utf7] = [$s_utf8]" ) ; + } +} + +# Copied from http://cpansearch.perl.org/src/FABPOT/Unicode-IMAPUtf7-2.01/lib/Unicode/IMAPUtf7.pm +# and then fixed with +# https://rt.cpan.org/Public/Bug/Display.html?id=11172 +sub imap_utf7_decode_old +{ + my ( $s ) = shift ; + + # Algorithm + # On remplace , par / dans les BASE 64 (, entre & et -) + # On remplace les &, non suivi d'un - par + + # On remplace les &- par & + $s =~ s/&([^,&\-]*),([^,\-&]*)\-/&$1\/$2\-/xg ; + $s =~ s/&(?!\-)/\+/xg ; + $s =~ s/&\-/&/xg ; + return( Unicode::String::utf7( $s )->utf8 ) ; +} + + + + + +sub tests_jux_utf8 +{ + note( 'Entering tests_jux_utf8()' ) ; + #no utf8 ; + use utf8 ; + + #binmode STDOUT, ":encoding(UTF-8)" ; + binmode STDERR, ":encoding(UTF-8)" ; + + # This test is because the binary can fail on it, a PAR.pm issue. + # The failure was with the underlying Encode::IMAPUTF7 module line 66 release 1.05 + # Was solved by including Encode in imapsync and using "pp -x". + ok( find_encoding( "UTF-16BE"), 'jux_utf8: Encode::find_encoding: UTF-16BE' ) ; + + # + is( '[]', jux_utf8( q{} ), 'jux_utf8: void => []' ) ; + is( '[INBOX]', jux_utf8( 'INBOX'), 'jux_utf8: INBOX => [INBOX]' ) ; + is( '[&ANY-] = [Ö]', jux_utf8( '&ANY-'), 'jux_utf8: &ANY- => [&ANY-] = [Ö]' ) ; + # +BD8EQAQ1BDQEOwQ+BDM- must stay as is + is( '[+BD8EQAQ1BDQEOwQ+BDM-]', jux_utf8( '+BD8EQAQ1BDQEOwQ+BDM-' ), 'jux_utf8: => [+BD8EQAQ1BDQEOwQ+BDM-] = [+BD8EQAQ1BDQEOwQ+BDM-]' ) ; + is( '[&BB8EQAQ+BDUEOgRC-] = [ßрþõúт]', jux_utf8( '&BB8EQAQ+BDUEOgRC-' ), 'jux_utf8: => [&BB8EQAQ+BDUEOgRC-] = [ßрþõúт]' ) ; + + is( '[R&AOk-ponses 1200+1201+1202] = [Réponses 1200+1201+1202]', jux_utf8( q{R&AOk-ponses 1200+1201+1202} ), 'jux_utf8: [R&AOk-ponses 1200+1201+1202] = [Réponses 1200+1201+1202]' ) ; + my $str = Encode::IMAPUTF7::encode("IMAP-UTF-7", 'Réponses 1200+1201+1202' ) ; + is( '[R&AOk-ponses 1200+1201+1202] = [Réponses 1200+1201+1202]', jux_utf8( $str ), "jux_utf8: [$str] = [Réponses 1200+1201+1202]" ) ; + + is( '[INBOX.&AOkA4ADnAPk-&-*] = [INBOX.éà çù&*]', jux_utf8( 'INBOX.&AOkA4ADnAPk-&-*' ), "jux_utf8: [INBOX.&AOkA4ADnAPk-&-*] = [INBOX.éà çù&*]" ) ; + + is( '[&ZTZO9nux-] = [æâ€Â¶Ã¤Â»Â¶Ã§Â®Â±]', jux_utf8( '&ZTZO9nux-'), 'jux_utf8: => [&ZTZO9nux-] = [æâ€Â¶Ã¤Â»Â¶Ã§Â®Â±]' ) ; + # + # + is( '[!Old Emails]', jux_utf8( '!Old Emails'), 'jux_utf8: !Old Emails => [!Old Emails]' ) ; + is( '[2006 Budget & Fcst]', jux_utf8( '2006 Budget & Fcst'), 'jux_utf8: 2006 Budget & Fcst => [2006 Budget & Fcst]' ) ; + note( 'Leaving tests_jux_utf8()' ) ; + return ; +} + +sub jux_utf8 +{ + #use utf8 ; + # juxtapose utf8 at the right if different + my ( $s_utf7 ) = shift ; + my ( $s_utf8 ) = imap_utf7_decode( $s_utf7 ) ; + + if ( $s_utf7 eq $s_utf8 ) { + #myprint( "[$s_utf7]\n" ) ; + return( "[$s_utf7]" ) ; + }else{ + #myprint( "[$s_utf7] = [$s_utf8]\n" ) ; + return( "[$s_utf7] = [$s_utf8]" ) ; + } +} + +sub imap_utf7_decode +{ + #use utf8 ; + my ( $s ) = shift ; + return( Encode::IMAPUTF7::decode("IMAP-UTF-7", $s ) ) ; +} + +sub imap_utf7_encode +{ + #use utf8 ; + my ( $s ) = shift ; + return( Encode::IMAPUTF7::encode("IMAP-UTF-7", $s ) ) ; +} + + + +sub imap_utf7_encode_old +{ + my ( $s ) = @_ ; + + $s = Unicode::String::utf8( $s )->utf7 ; + + $s =~ s/\+([^\/&\-]*)\/([^\/\-&]*)\-/\+$1,$2\-/xg ; + $s =~ s/&/&\-/xg ; + $s =~ s/\+([^+\-]+)?\-/&$1\-/xg ; + return( $s ) ; +} + + + + +sub select_folder +{ + my ( $mysync, $imap, $folder, $hostside ) = @_ ; + if ( ! $imap->select( $folder ) ) { + my $error = join q{}, + "$hostside folder $folder: Could not select: ", + $imap->LastError, "\n" ; + errors_incr( $mysync, $error ) ; + return( 0 ) ; + }else{ + # ok select succeeded + return( 1 ) ; + } +} + +sub examine_folder +{ + my ( $mysync, $imap, $folder, $hostside ) = @_ ; + if ( ! $imap->examine( $folder ) ) { + my $error = join q{}, + "$hostside folder $folder: Could not examine: ", + $imap->LastError, "\n" ; + errors_incr( $mysync, $error ) ; + return( 0 ) ; + }else{ + # ok select succeeded + return( 1 ) ; + } +} + + +sub count_from_select +{ + my @lines = @ARG ; + my $count ; + foreach my $line ( @lines ) { + #myprint( "line = [$line]\n" ) ; + if ( $line =~ m/^\*\s+(\d+)\s+EXISTS/x ) { + $count = $1 ; + return( $count ) ; + } + } + return( undef ) ; +} + + + +sub create_folder_old +{ + my $mysync = shift @ARG ; + my( $imap, $h2_fold, $h1_fold ) = @ARG ; + + myprint( "Creating (old way) folder [$h2_fold] on host2\n" ) ; + if ( ( 'INBOX' eq uc $h2_fold ) + and ( $imap->exists( $h2_fold ) ) ) { + myprint( "Folder [$h2_fold] already exists\n" ) ; + return( 1 ) ; + } + if ( ! $mysync->{dry} ){ + if ( ! $imap->create( $h2_fold ) ) { + my $error = join q{}, + "Could not create folder [$h2_fold] from [$h1_fold]: ", + $imap->LastError( ), "\n" ; + errors_incr( $mysync, $error ) ; + # success if folder exists ("already exists" error) + return( 1 ) if $imap->exists( $h2_fold ) ; + # failure since create failed + return( 0 ) ; + }else{ + #create succeeded + myprint( "Created ( the old way ) folder [$h2_fold] on host2\n" ) ; + return( 1 ) ; + } + }else{ + # dry mode, no folder so many imap will fail, assuming failure + myprint( "Created ( the old way ) folder [$h2_fold] on host2 $mysync->{dry_message}\n" ) ; + return( 0 ) ; + } +} + + +sub create_folder +{ + my $mysync = shift @ARG ; + my( $myimap2 , $h2_fold , $h1_fold ) = @ARG ; + my( @parts , $parent ) ; + + if ( $myimap2->IsUnconnected( ) ) { + myprint( "Host2: Unconnected state\n" ) ; + return( 0 ) ; + } + + if ( $create_folder_old ) { + return( create_folder_old( $mysync, $myimap2 , $h2_fold , $h1_fold ) ) ; + } + + # $imap->exists() calls $imap->status() that does an IMAP STATUS folder + myprint( "Creating folder [$h2_fold] on host2\n" ) ; + if ( ( 'INBOX' eq uc $h2_fold ) + and ( $myimap2->exists( $h2_fold ) ) ) { + myprint( "Folder [$h2_fold] already exists\n" ) ; + return( 1 ) ; + } + + if ( $mixfolders and $myimap2->exists( $h2_fold ) ) { + myprint( "Folder [$h2_fold] already exists (--nomixfolders is not set)\n" ) ; + return( 1 ) ; + } + + + if ( ( not $mixfolders ) and ( $myimap2->exists( $h2_fold ) ) ) { + myprint( "Folder [$h2_fold] already exists and --nomixfolders is set\n" ) ; + return( 0 ) ; + } + + @parts = split /\Q$mysync->{ h2_sep }\E/x, $h2_fold ; + pop @parts ; + $parent = join $mysync->{ h2_sep }, @parts ; + $parent =~ s/^\s+|\s+$//xg ; + if ( ( $parent ne q{} ) and ( ! $myimap2->exists( $parent ) ) ) { + create_folder( $mysync, $myimap2 , $parent , $h1_fold ) ; + } + + if ( ! $mysync->{dry} ) { + if ( ! $myimap2->create( $h2_fold ) ) { + my $error = join q{}, + "Could not create folder [$h2_fold] from [$h1_fold]: " , + $myimap2->LastError( ), "\n" ; + errors_incr( $mysync, $error ) ; + # success if folder exists ("already exists" error) or selectable + if ( $myimap2->exists( $h2_fold ) or select_folder( $mysync, $myimap2, $h2_fold, 'Host2' ) ) + { + return( 1 ) ; + } + # failure since create failed + not exist + not selectable + return( 0 ) ; + }else{ + #create succeeded + myprint( "Created folder [$h2_fold] on host2\n" ) ; + return( 1 ) ; + } + }else{ + # dry mode, no folder so many imap will fail, assuming failure + myprint( "Created folder [$h2_fold] on host2 $mysync->{dry_message}\n" ) ; + if ( ! $mysync->{ justfolders } ) { + myprint( "Since --dry mode is on and folder [$h2_fold] on host2 does not exist yet, syncing messages will not be simulated.\n" + . "To simulate message syncing, use --justfolders without --dry to first create the missing folders then rerun the --dry sync.\n" ) ; + # The messages that could be transferred are counted and the number is given at the end. + } + return( 0 ) ; + } +} + + + +sub tests_folder_routines +{ + note( 'Entering tests_folder_routines()' ) ; + + ok( !is_requested_folder('folder_foo'), 'is_requested_folder folder_foo 1' ); + ok( add_to_requested_folders('folder_foo'), 'add_to_requested_folders folder_foo' ); + ok( is_requested_folder('folder_foo'), 'is_requested_folder folder_foo 2' ); + ok( !is_requested_folder('folder_NO_EXIST'), 'is_requested_folder folder_NO_EXIST' ); + + is_deeply( [ 'folder_foo' ], [ remove_from_requested_folders( 'folder_foo' ) ], 'removed folder_foo => folder_foo' ) ; + ok( !is_requested_folder('folder_foo'), 'is_requested_folder folder_foo 3' ); + my @f ; + ok( @f = add_to_requested_folders('folder_bar', 'folder_toto'), "add result: @f" ); + ok( is_requested_folder('folder_bar'), 'is_requested_folder 4' ); + ok( is_requested_folder('folder_toto'), 'is_requested_folder 5' ); + ok( remove_from_requested_folders('folder_toto'), 'remove_from_requested_folders: ' ); + ok( !is_requested_folder('folder_toto'), 'is_requested_folder 6' ); + + is_deeply( [ 'folder_bar' ], [ remove_from_requested_folders('folder_bar') ], 'remove_from_requested_folders: empty' ) ; + + ok( 0 == compare_lists( [ sort_requested_folders( ) ], [] ), 'sort_requested_folders: all empty' ) ; + ok( add_to_requested_folders( 'A_99', 'M_55', 'Z_11' ), 'add_to_requested_folders M_55 Z_11' ); + ok( 0 == compare_lists( [ sort_requested_folders( ) ], [ 'A_99', 'M_55', 'Z_11' ] ), 'sort_requested_folders: middle' ) ; + + + @folderfirst = ( 'Z_11' ) ; + + ok( 0 == compare_lists( [ sort_requested_folders( ) ], [ 'Z_11', 'A_99', 'M_55' ] ), 'sort_requested_folders: first+middle' ) ; + + is_deeply( [ 'Z_11', 'A_99', 'M_55' ], [ sort_requested_folders( ) ], 'sort_requested_folders: first+middle is_deeply' ) ; + + @folderlast = ( 'A_99' ) ; + ok( 0 == compare_lists( [ sort_requested_folders( ) ], [ 'Z_11', 'M_55', 'A_99' ] ), 'sort_requested_folders: first+middle+last 1' ) ; + + ok( add_to_requested_folders('M_55', 'M_44',), 'add_to_requested_folders M_55 M_44' ) ; + + ok( 0 == compare_lists( [ sort_requested_folders( ) ], [ 'Z_11', 'M_44', 'M_55', 'A_99'] ), 'sort_requested_folders: first+middle+last 2' ) ; + + + ok( add_to_requested_folders('A_88', 'Z_22',), 'add_to_requested_folders A_88 Z_22' ) ; + @folderfirst = qw( Z_22 Z_11 ) ; + @folderlast = qw( A_99 A_88 ) ; + ok( 0 == compare_lists( [ sort_requested_folders( ) ], [ 'Z_22', 'Z_11', 'M_44', 'M_55', 'A_99', 'A_88' ] ), 'sort_requested_folders: first+middle+last 3' ) ; + undef @folderfirst ; + undef @folderlast ; + + note( 'Leaving tests_folder_routines()' ) ; + return ; +} + + +sub sort_requested_folders +{ + my @requested_folders_sorted = () ; + + $sync->{ debug } and myprint "folderfirst: @folderfirst\n" ; + my @folderfirst_requested = remove_from_requested_folders( @folderfirst ) ; + #myprint "folderfirst_requested: @folderfirst_requested\n" ; + + my @folderlast_requested = remove_from_requested_folders( @folderlast ) ; + + my @middle = sort keys %requested_folder ; + + @requested_folders_sorted = ( @folderfirst_requested, @middle, @folderlast_requested ) ; + $sync->{ debug } and myprint "requested_folders_sorted: @requested_folders_sorted\n" ; + add_to_requested_folders( @requested_folders_sorted ) ; + + return( @requested_folders_sorted ) ; +} + +sub is_requested_folder +{ + my ( $folder ) = @_; + + return( defined $requested_folder{ $folder } ) ; +} + + +sub add_to_requested_folders +{ + my @wanted_folders = @_ ; + + foreach my $folder ( @wanted_folders ) { + ++$requested_folder{ $folder } ; + } + return( keys %requested_folder ) ; +} + +sub tests_remove_from_requested_folders +{ + note( 'Entering tests_remove_from_requested_folders()' ) ; + + is( undef, undef, 'remove_from_requested_folders: undef is undef' ) ; + is_deeply( [], [ remove_from_requested_folders( ) ], 'remove_from_requested_folders: no args' ) ; + %requested_folder = ( + 'F1' => 1, + ) ; + is_deeply( [], [ remove_from_requested_folders( ) ], 'remove_from_requested_folders: remove nothing among F1 => nothing' ) ; + is_deeply( [], [ remove_from_requested_folders( 'Fno' ) ], 'remove_from_requested_folders: remove Fno among F1 => nothing' ) ; + is_deeply( [ 'F1' ], [ remove_from_requested_folders( 'F1' ) ], 'remove_from_requested_folders: remove F1 among F1 => F1' ) ; + is_deeply( { }, { %requested_folder }, 'remove_from_requested_folders: remove F1 among F1 => %requested_folder emptied' ) ; + + %requested_folder = ( + 'F1' => 1, + 'F2' => 1, + ) ; + is_deeply( [], [ remove_from_requested_folders( ) ], 'remove_from_requested_folders: remove nothing among F1 F2 => nothing' ) ; + is_deeply( [], [ remove_from_requested_folders( 'Fno' ) ], 'remove_from_requested_folders: remove Fno among F1 F2 => nothing' ) ; + is_deeply( [ 'F1' ], [ remove_from_requested_folders( 'F1' ) ], 'remove_from_requested_folders: remove F1 among F1 F2 => F1' ) ; + is_deeply( { 'F2' => 1 }, { %requested_folder }, 'remove_from_requested_folders: remove F1 among F1 F2 => %requested_folder F2' ) ; + + is_deeply( [], [ remove_from_requested_folders( 'F1' ) ], 'remove_from_requested_folders: remove F1 among F2 => nothing' ) ; + is_deeply( [ 'F2' ], [ remove_from_requested_folders( 'F1', 'F2' ) ], 'remove_from_requested_folders: remove F1 F2 among F2 => F2' ) ; + is_deeply( {}, { %requested_folder }, 'remove_from_requested_folders: remove F1 among F1 F2 => %requested_folder F2' ) ; + + %requested_folder = ( + 'F1' => 1, + 'F2' => 1, + 'F3' => 1, + ) ; + is_deeply( [ 'F1', 'F2' ], [ remove_from_requested_folders( 'F1', 'F2' ) ], 'remove_from_requested_folders: remove F1 F2 among F1 F2 F3 => F1 F2' ) ; + is_deeply( { 'F3' => 1 }, { %requested_folder }, 'remove_from_requested_folders: remove F1 F2 among F1 F2 F3 => %requested_folder F3' ) ; + + undef %requested_folder ; + + note( 'Leaving tests_remove_from_requested_folders()' ) ; + return ; +} + + +sub remove_from_requested_folders +{ + my @unwanted_folders = @_ ; + + my @removed_folders = () ; + foreach my $folder ( @unwanted_folders ) { + if ( exists $requested_folder{ $folder } ) + { + delete $requested_folder{ $folder } ; + push @removed_folders, $folder ; + } + } + return( @removed_folders ) ; +} + +sub compare_lists +{ + my ($list_1_ref, $list_2_ref) = @_; + + return($MINUS_ONE) if ((not defined $list_1_ref) and defined $list_2_ref); + return(0) if ((not defined $list_1_ref) and not defined $list_2_ref); # end if no list + return(1) if (not defined $list_2_ref); # end if only one list + + if (not ref $list_1_ref ) {$list_1_ref = [$list_1_ref]}; + if (not ref $list_2_ref ) {$list_2_ref = [$list_2_ref]}; + + + my $last_used_indice = $MINUS_ONE; + + + ELEMENT: + foreach my $indice ( 0 .. $#{ $list_1_ref } ) { + $last_used_indice = $indice ; + + # End of list_2 + return 1 if ($indice > $#{ $list_2_ref } ) ; + + my $element_list_1 = $list_1_ref->[$indice] ; + my $element_list_2 = $list_2_ref->[$indice] ; + my $balance = $element_list_1 cmp $element_list_2 ; + next ELEMENT if ($balance == 0) ; + return $balance ; + } + # each element equal until last indice of list_1 + return $MINUS_ONE if ($last_used_indice < $#{ $list_2_ref } ) ; + + # same size, each element equal + return 0 ; +} + +sub tests_compare_lists +{ + note( 'Entering tests_compare_lists()' ) ; + + my $empty_list_ref = []; + + ok( 0 == compare_lists() , 'compare_lists, no args'); + ok( 0 == compare_lists(undef) , 'compare_lists, undef = nothing'); + ok( 0 == compare_lists(undef, undef) , 'compare_lists, undef = undef'); + ok($MINUS_ONE == compare_lists(undef , []) , 'compare_lists, undef < []'); + ok($MINUS_ONE == compare_lists(undef , [1]) , 'compare_lists, undef < [1]'); + ok($MINUS_ONE == compare_lists(undef , [0]) , 'compare_lists, undef < [0]'); + ok(+1 == compare_lists([]) , 'compare_lists, [] > nothing'); + ok(+1 == compare_lists([], undef) , 'compare_lists, [] > undef'); + ok( 0 == compare_lists([] , []) , 'compare_lists, [] = []'); + + ok($MINUS_ONE == compare_lists([] , [1]) , 'compare_lists, [] < [1]'); + ok(+1 == compare_lists([1] , []) , 'compare_lists, [1] > []'); + + + ok( 0 == compare_lists( [1], 1 ) , 'compare_lists, [1] = 1 ') ; + ok( 0 == compare_lists( 1 , [1] ) , 'compare_lists, 1 = [1]') ; + ok( 0 == compare_lists( 1 , 1 ) , 'compare_lists, 1 = 1 ') ; + ok( $MINUS_ONE == compare_lists( 0 , 1 ) , 'compare_lists, 0 < 1 ') ; + ok( $MINUS_ONE == compare_lists( $MINUS_ONE , 0 ) , 'compare_lists, -1 < 0 ') ; + ok( $MINUS_ONE == compare_lists( 1 , 2 ) , 'compare_lists, 1 < 2 ') ; + ok( +1 == compare_lists( 2 , 1 ) , 'compare_lists, 2 > 1 ') ; + + + ok( 0 == compare_lists([1,2], [1,2]) , 'compare_lists, [1,2] = [1,2]' ) ; + ok($MINUS_ONE == compare_lists([1], [1,2]) , 'compare_lists, [1] < [1,2]' ) ; + ok(+1 == compare_lists([2], [1,2]) , 'compare_lists, [2] > [1,2]' ) ; + ok($MINUS_ONE == compare_lists([1], [1,1]) , 'compare_lists, [1] < [1,1]' ) ; + ok(+1 == compare_lists([1, 1], [1]) , 'compare_lists, [1, 1] > [1]' ) ; + ok( 0 == compare_lists([1 .. $NUMBER_20_000] , [1 .. $NUMBER_20_000]) + , 'compare_lists, [1..20_000] = [1..20_000]' ) ; + ok($MINUS_ONE == compare_lists([1], [2]) , 'compare_lists, [1] < [2]') ; + ok( 0 == compare_lists([2], [2]) , 'compare_lists, [0] = [2]') ; + ok(+1 == compare_lists([2], [1]) , 'compare_lists, [2] > [1]') ; + + ok($MINUS_ONE == compare_lists(['a'], ['b']) , 'compare_lists, ["a"] < ["b"]') ; + ok( 0 == compare_lists(['a'], ['a']) , 'compare_lists, ["a"] = ["a"]') ; + ok( 0 == compare_lists(['ab'], ['ab']) , 'compare_lists, ["ab"] = ["ab"]') ; + ok(+1 == compare_lists(['b'], ['a']) , 'compare_lists, ["b"] > ["a"]') ; + ok($MINUS_ONE == compare_lists(['a'], ['aa']) , 'compare_lists, ["a"] < ["aa"]') ; + ok($MINUS_ONE == compare_lists(['a'], ['a', 'a']), 'compare_lists, ["a"] < ["a", "a"]') ; + ok( 0 == compare_lists([split q{ }, 'a b' ], ['a', 'b']), 'compare_lists, split') ; + ok( 0 == compare_lists([sort split q{ }, 'b a' ], ['a', 'b']), 'compare_lists, sort split') ; + + note( 'Leaving tests_compare_lists()' ) ; + return ; +} + + +sub guess_prefix +{ + my @foldernames = @_ ; + + my $prefix_guessed = q{} ; + foreach my $folder ( @foldernames ) { + next if ( $folder =~ m{^INBOX$}xi ) ; # no guessing from INBOX + if ( $folder !~ m{^INBOX}xi ) { + $prefix_guessed = q{} ; # prefix empty guessed + last ; + } + if ( $folder =~ m{^(INBOX(?:\.|\/))}xi ) { + $prefix_guessed = $1 ; # prefix Inbox/ or INBOX. guessed + } + } + return( $prefix_guessed ) ; +} + +sub tests_guess_prefix +{ + note( 'Entering tests_guess_prefix()' ) ; + + is( guess_prefix( ), q{}, 'guess_prefix: no args => empty string' ) ; + is( q{} , guess_prefix( 'INBOX' ), 'guess_prefix: INBOX alone' ) ; + is( q{} , guess_prefix( 'Inbox' ), 'guess_prefix: Inbox alone' ) ; + is( q{} , guess_prefix( 'INBOX' ), 'guess_prefix: INBOX alone' ) ; + is( 'INBOX/' , guess_prefix( 'INBOX', 'INBOX/Junk' ), 'guess_prefix: INBOX INBOX/Junk' ) ; + is( 'INBOX.' , guess_prefix( 'INBOX', 'INBOX.Junk' ), 'guess_prefix: INBOX INBOX.Junk' ) ; + is( 'Inbox/' , guess_prefix( 'Inbox', 'Inbox/Junk' ), 'guess_prefix: Inbox Inbox/Junk' ) ; + is( 'Inbox.' , guess_prefix( 'Inbox', 'Inbox.Junk' ), 'guess_prefix: Inbox Inbox.Junk' ) ; + is( 'INBOX/' , guess_prefix( 'INBOX', 'INBOX/Junk', 'INBOX/rrr' ), 'guess_prefix: INBOX INBOX/Junk INBOX/rrr' ) ; + is( q{} , guess_prefix( 'INBOX', 'INBOX/Junk', 'INBOX/rrr', 'zzz' ), 'guess_prefix: INBOX INBOX/Junk INBOX/rrr zzz' ) ; + is( q{} , guess_prefix( 'INBOX', 'Junk' ), 'guess_prefix: INBOX Junk' ) ; + is( q{} , guess_prefix( 'INBOX', 'Junk' ), 'guess_prefix: INBOX Junk' ) ; + + note( 'Leaving tests_guess_prefix()' ) ; + return ; +} + +sub get_prefix +{ + my( $imap, $prefix_in, $prefix_opt, $Side, $folders_ref ) = @_ ; + my( $prefix_out, $prefix_guessed ) ; + + ( $sync->{ debug } or $sync->{debugfolders} ) and myprint( "$Side: Getting prefix\n" ) ; + $prefix_guessed = guess_prefix( @{ $folders_ref } ) ; + myprint( "$Side: guessing prefix from folder listing: [$prefix_guessed]\n" ) ; + ( $sync->{ debug } or $sync->{debugfolders} ) and myprint( "$Side: Calling namespace capability\n" ) ; + if ( $imap->has_capability( 'namespace' ) ) { + my $r_namespace = $imap->namespace( ) ; + $prefix_out = $r_namespace->[0][0][0] ; + myprint( "$Side: prefix given by NAMESPACE: [$prefix_out]\n" ) ; + if ( defined $prefix_in ) { + myprint( "$Side: but using [$prefix_in] given by $prefix_opt\n" ) ; + $prefix_out = $prefix_in ; + return( $prefix_out ) ; + }else{ + # all good + return( $prefix_out ) ; + } + } + else{ + if ( defined $prefix_in ) { + myprint( "$Side: using [$prefix_in] given by $prefix_opt\n" ) ; + $prefix_out = $prefix_in ; + return( $prefix_out ) ; + }else{ + myprint( + "$Side: No NAMESPACE capability so using guessed prefix [$prefix_guessed]\n", + help_to_guess_prefix( $imap, $prefix_opt ) ) ; + return( $prefix_guessed ) ; + } + } + return ; +} + + +sub guess_separator +{ + my @foldernames = @_ ; + + #return( undef ) unless ( @foldernames ) ; + + my $sep_guessed ; + my %counter ; + foreach my $folder ( @foldernames ) { + $counter{'/'}++ while ( $folder =~ m{/}xg ) ; # count / + $counter{'.'}++ while ( $folder =~ m{\.}xg ) ; # count . + $counter{'\\\\'}++ while ( $folder =~ m{(\\){2}}xg ) ; # count \\ + $counter{'\\'}++ while ( $folder =~ m{[^\\](\\){1}(?=[^\\])}xg ) ; # count \ + } + my @race_sorted = sort { $counter{ $b } <=> $counter{ $a } } keys %counter ; + $sync->{ debug } and myprint( "@foldernames\n@race_sorted\n", %counter, "\n" ) ; + $sep_guessed = shift @race_sorted || $LAST_RESSORT_SEPARATOR ; # / when nothing found. + return( $sep_guessed ) ; +} + +sub tests_guess_separator +{ + note( 'Entering tests_guess_separator()' ) ; + + ok( '/' eq guess_separator( ), 'guess_separator: no args' ) ; + ok( '/' eq guess_separator( 'abcd' ), 'guess_separator: abcd' ) ; + ok( '/' eq guess_separator( 'a/b/c.d' ), 'guess_separator: a/b/c.d' ) ; + ok( '.' eq guess_separator( 'a.b/c.d' ), 'guess_separator: a.b/c.d' ) ; + ok( '\\\\' eq guess_separator( 'a\\\\b\\\\c.c\\\\d/e/f' ), 'guess_separator: a\\\\b\\\\c.c\\\\d/e/f' ) ; + ok( '\\' eq guess_separator( 'a\\b\\c.c\\d/e/f' ), 'guess_separator: a\\b\\c.c\\d/e/f' ) ; + ok( '\\' eq guess_separator( 'a\\b' ), 'guess_separator: a\\b' ) ; + ok( '\\' eq guess_separator( 'a\\b\\c' ), 'guess_separator: a\\b\\c' ) ; + + note( 'Leaving tests_guess_separator()' ) ; + return ; +} + +sub get_separator +{ + my( $imap, $sep_in, $sep_opt, $Side, $folders_ref ) = @_ ; + my( $sep_out, $sep_guessed ) ; + + ( $sync->{ debug } or $sync->{debugfolders} ) and myprint( "$Side: Getting separator\n" ) ; + $sep_guessed = guess_separator( @{ $folders_ref } ) ; + myprint( "$Side: guessing separator from folder listing: [$sep_guessed]\n" ) ; + + ( $sync->{ debug } or $sync->{debugfolders} ) and myprint( "$Side: calling namespace capability\n" ) ; + if ( $imap->has_capability( 'namespace' ) ) + { + $sep_out = $imap->separator( ) ; + if ( defined $sep_out ) { + myprint( "$Side: separator given by NAMESPACE: [$sep_out]\n" ) ; + if ( defined $sep_in ) { + myprint( "$Side: but using [$sep_in] given by $sep_opt\n" ) ; + $sep_out = $sep_in ; + return( $sep_out ) ; + }else{ + return( $sep_out ) ; + } + }else{ + if ( defined $sep_in ) { + myprint( "$Side: NAMESPACE request failed but using [$sep_in] given by $sep_opt\n" ) ; + $sep_out = $sep_in ; + return( $sep_out ) ; + }else{ + myprint( + "$Side: NAMESPACE request failed so using guessed separator [$sep_guessed]\n", + help_to_guess_sep( $imap, $sep_opt ) ) ; + return( $sep_guessed ) ; + } + } + } + else + { + if ( defined $sep_in ) { + myprint( "$Side: No NAMESPACE capability but using [$sep_in] given by $sep_opt\n" ) ; + $sep_out = $sep_in ; + return( $sep_out ) ; + }else{ + myprint( + "$Side: No NAMESPACE capability, so using guessed separator [$sep_guessed]\n", + help_to_guess_sep( $imap, $sep_opt ) ) ; + return( $sep_guessed ) ; + } + } + return ; +} + +sub help_to_guess_sep +{ + my( $imap, $sep_opt ) = @_ ; + + my $help_to_guess_sep = "You can set the separator character with the $sep_opt option,\n" + . "the complete listing of folders may help you to find it\n" + . folders_list_to_help( $imap ) ; + + return( $help_to_guess_sep ) ; +} + +sub help_to_guess_prefix +{ + my( $imap, $prefix_opt ) = @_ ; + + my $help_to_guess_prefix = "You can set the prefix namespace with the $prefix_opt option,\n" + . "the folowing listing of folders may help you to find it:\n" + . folders_list_to_help( $imap ) ; + + return( $help_to_guess_prefix ) ; +} + + +sub folders_list_to_help +{ + my( $imap ) = shift ; + + my @folders = $imap->folders ; + my $listing = join q{}, map { "[$_]\n" } @folders ; + return( $listing ) ; +} + +# Globals are $sync @h1_folders_all @h2_folders_all $prefix1 $prefix2 +sub private_folders_separators_and_prefixes +{ +# what are the private folders separators and prefixes for each server ? + + ( $sync->{ debug } or $sync->{debugfolders} ) and myprint( "Getting separators\n" ) ; + $sync->{ h1_sep } = get_separator( $sync->{imap1}, $sync->{ sep1 }, '--sep1', 'Host1', \@h1_folders_all ) ; + $sync->{ h2_sep } = get_separator( $sync->{imap2}, $sync->{ sep2 }, '--sep2', 'Host2', \@h2_folders_all ) ; + + + $sync->{ h1_prefix } = get_prefix( $sync->{imap1}, $prefix1, '--prefix1', 'Host1', \@h1_folders_all ) ; + $sync->{ h2_prefix } = get_prefix( $sync->{imap2}, $prefix2, '--prefix2', 'Host2', \@h2_folders_all ) ; + + myprint( "Host1: separator and prefix: [$sync->{ h1_sep }][$sync->{ h1_prefix }]\n" ) ; + myprint( "Host2: separator and prefix: [$sync->{ h2_sep }][$sync->{ h2_prefix }]\n" ) ; + return ; +} + + +sub subfolder1 +{ + my $mysync = shift ; + my $subfolder1 = sanitize_subfolder( $mysync->{ subfolder1 } ) ; + + if ( $subfolder1 ) + { + # turns off automap + myprint( "Turning off automapping folders because of --subfolder1\n" ) ; + $mysync->{ automap } = undef ; + myprint( "Sanitizing subfolder1: [$mysync->{ subfolder1 }] => [$subfolder1]\n" ) ; + $mysync->{ subfolder1 } = $subfolder1 ; + if ( ! add_subfolder1_to_folderrec( $mysync ) ) + { + $mysync->{nb_errors}++ ; + exit_clean( $mysync, $EXIT_SUBFOLDER1_NO_EXISTS, "subfolder1 $subfolder1 does not exist\n" ) ; + } + } + else + { + $mysync->{ subfolder1 } = undef ; + } + + return ; +} + +sub subfolder2 +{ + my $mysync = shift ; + my $subfolder2 = sanitize_subfolder( $mysync->{ subfolder2 } ) ; + if ( $subfolder2 ) + { + # turns off automap + myprint( "Turning off automapping folders because of --subfolder2\n" ) ; + $mysync->{ automap } = undef ; + myprint( "Sanitizing subfolder2: [$mysync->{ subfolder2 }] => [$subfolder2]\n" ) ; + $mysync->{ subfolder2 } = $subfolder2 ; + set_regextrans2_for_subfolder2( $mysync ) ; + } + else + { + $mysync->{ subfolder2 } = undef ; + } + + return ; +} + +sub tests_sanitize_subfolder +{ + note( 'Entering tests_sanitize_subfolder()' ) ; + + is( undef, sanitize_subfolder( ), 'sanitize_subfolder: no args => undef' ) ; + is( undef, sanitize_subfolder( q{} ), 'sanitize_subfolder: empty => undef' ) ; + is( undef, sanitize_subfolder( ' ' ), 'sanitize_subfolder: blank => undef' ) ; + is( undef, sanitize_subfolder( ' ' ), 'sanitize_subfolder: blanks => undef' ) ; + is( 'abcd', sanitize_subfolder( 'abcd' ), 'sanitize_subfolder: abcd => abcd' ) ; + is( 'ab cd', sanitize_subfolder( ' ab cd ' ), 'sanitize_subfolder: " ab cd " => "ab cd"' ) ; + is( 'abcd', sanitize_subfolder( q{a&~b#\\c[]=d;} ), 'sanitize_subfolder: "a&~b#\\c[]=d;" => "abcd"' ) ; + is( 'aA.b-_ 8c/dD', sanitize_subfolder( 'aA.b-_ 8c/dD' ), 'sanitize_subfolder: aA.b-_ 8c/dD => aA.b-_ 8c/dD' ) ; + note( 'Leaving tests_sanitize_subfolder()' ) ; + return ; +} + + +sub sanitize_subfolder +{ + my $subfolder = shift ; + + if ( ! $subfolder ) + { + return ; + } + # Remove edging blanks + $subfolder =~ s,^ +| +$,,g ; + # Keep only abcd...ABCD...0123... and -_./ + $subfolder =~ tr,-_a-zA-Z0-9./ ,,cd ; + + # A blank subfolder is not a subfolder + if ( ! $subfolder ) + { + return ; + } + else + { + return $subfolder ; + } +} + +sub tests_sanitize_host +{ + note( 'Entering tests_sanitize_host()' ) ; + + is( undef, sanitize_host( ), 'sanitize_host: no args => undef' ) ; + is( '', sanitize_host( '' ), 'sanitize_host: empty => empty' ) ; + is( 'imap.example.org', sanitize_host( 'imap.example.org' ), 'sanitize_host: imap.example.org => imap.example.org' ) ; + is( 'imap.example.org', sanitize_host( ' imap.example.org' ), 'sanitize_host: imap.example.org 1 => imap.example.org' ) ; + is( 'imap.example.org', sanitize_host( 'imap.example.org ' ), 'sanitize_host: imap.example.org 2 => imap.example.org' ) ; + is( 'imap.example.org', sanitize_host( 'imap.exam ple.org' ), 'sanitize_host: imap.example.org 3 => imap.example.org' ) ; + is( 'imap.example.org', sanitize_host( ' imap.exam ple.org ' ), 'sanitize_host: imap.example.org 4 => imap.example.org' ) ; + is( 'imap.example.org', sanitize_host( 'imap.exa/mple.org/' ), 'sanitize_host: imap.example.org/ => imap.example.org' ) ; + + note( 'Leaving tests_sanitize_host()' ) ; + return ; +} + + +sub sanitize_host +{ + my $host = shift ; + if ( ! defined $host ) { return ; } + + $host =~ tr{ /}{}d ; + return $host ; +} + + +sub tests_add_subfolder1_to_folderrec +{ + note( 'Entering tests_add_subfolder1_to_folderrec()' ) ; + + is( undef, add_subfolder1_to_folderrec( ), 'add_subfolder1_to_folderrec: undef => undef' ) ; + is_deeply( [], [ add_subfolder1_to_folderrec( ) ], 'add_subfolder1_to_folderrec: no args => empty array' ) ; + @folderrec = () ; + my $mysync = {} ; + is_deeply( [ ], [ add_subfolder1_to_folderrec( $mysync ) ], 'add_subfolder1_to_folderrec: empty => empty array' ) ; + is_deeply( [ ], [ @folderrec ], 'add_subfolder1_to_folderrec: empty => empty folderrec' ) ; + $mysync->{ subfolder1 } = 'SUBI' ; + $h1_folders_all{ 'SUBI' } = 1 ; + $mysync->{ h1_prefix } = 'INBOX/' ; + is_deeply( [ 'SUBI' ], [ add_subfolder1_to_folderrec( $mysync ) ], 'add_subfolder1_to_folderrec: SUBI => SUBI' ) ; + is_deeply( [ 'SUBI' ], [ @folderrec ], 'add_subfolder1_to_folderrec: SUBI => folderrec SUBI ' ) ; + + @folderrec = () ; + $mysync->{ subfolder1 } = 'SUBO' ; + is_deeply( [ ], [ add_subfolder1_to_folderrec( $mysync ) ], 'add_subfolder1_to_folderrec: SUBO no exists => empty array' ) ; + is_deeply( [ ], [ @folderrec ], 'add_subfolder1_to_folderrec: SUBO no exists => empty folderrec' ) ; + $h1_folders_all{ 'INBOX/SUBO' } = 1 ; + is_deeply( [ 'INBOX/SUBO' ], [ add_subfolder1_to_folderrec( $mysync ) ], 'add_subfolder1_to_folderrec: SUBO + INBOX/SUBO exists => INBOX/SUBO' ) ; + is_deeply( [ 'INBOX/SUBO' ], [ @folderrec ], 'add_subfolder1_to_folderrec: SUBO + INBOX/SUBO exists => INBOX/SUBO folderrec' ) ; + + note( 'Leaving tests_add_subfolder1_to_folderrec()' ) ; + return ; +} + + +sub add_subfolder1_to_folderrec +{ + my $mysync = shift ; + if ( ! $mysync || ! $mysync->{ subfolder1 } ) + { + return ; + } + + my $subfolder1 = $mysync->{ subfolder1 } ; + my $subfolder1_extended = $mysync->{ h1_prefix } . $subfolder1 ; + + if ( exists $h1_folders_all{ $subfolder1 } ) + { + myprint( qq{Acting like --folderrec "$subfolder1"\n} ) ; + push @folderrec, $subfolder1 ; + } + elsif ( exists $h1_folders_all{ $subfolder1_extended } ) + { + myprint( qq{Acting like --folderrec "$subfolder1_extended"\n} ) ; + push @folderrec, $subfolder1_extended ; + } + else + { + myprint( qq{Nor folder "$subfolder1" nor "$subfolder1_extended" exists on host1\n} ) ; + } + return @folderrec ; +} + +sub set_regextrans2_for_subfolder2 +{ + my $mysync = shift ; + + + unshift @{ $mysync->{ regextrans2 } }, + q(s,^$mysync->{ h2_prefix }(.*),$mysync->{ h2_prefix }$mysync->{ subfolder2 }$mysync->{ h2_sep }$1,), + q(s,^INBOX$,$mysync->{ h2_prefix }$mysync->{ subfolder2 }$mysync->{ h2_sep }INBOX,), + q(s,^($mysync->{ h2_prefix }){2},$mysync->{ h2_prefix },); + + #myprint( "@{ $mysync->{ regextrans2 } }\n" ) ; + return ; +} + + + +# Looks like no globals here + +sub tests_imap2_folder_name +{ + note( 'Entering tests_imap2_folder_name()' ) ; + + my $mysync = {} ; + $mysync->{ h1_prefix } = q{} ; + $mysync->{ h2_prefix } = q{} ; + $mysync->{ h1_sep } = '/'; + $mysync->{ h2_sep } = '.'; + + $mysync->{ debug } and myprint( <<"EOS" +prefix1: [$mysync->{ h1_prefix }] +prefix2: [$mysync->{ h2_prefix }] +sep1: [$sync->{ h1_sep }] +sep2: [$sync->{ h2_sep }] +EOS +) ; + + $mysync->{ fixslash2 } = 0 ; + is( q{INBOX}, imap2_folder_name( $mysync, q{} ), 'imap2_folder_name: empty string' ) ; + is( 'blabla', imap2_folder_name( $mysync, 'blabla' ), 'imap2_folder_name: blabla' ) ; + is('spam.spam', imap2_folder_name( $mysync, 'spam/spam' ), 'imap2_folder_name: spam/spam' ) ; + + is( 'spam/spam', imap2_folder_name( $mysync, 'spam.spam' ), 'imap2_folder_name: spam.spam') ; + is( 'spam.spam/spam', imap2_folder_name( $mysync, 'spam/spam.spam' ), 'imap2_folder_name: spam/spam.spam' ) ; + is( 's pam.spam/sp am', imap2_folder_name( $mysync, 's pam/spam.sp am' ), 'imap2_folder_name: s pam/spam.sp am' ) ; + + $mysync->{f1f2h}{ 'auto' } = 'moto' ; + is( 'moto', imap2_folder_name( $mysync, 'auto' ), 'imap2_folder_name: auto' ) ; + $mysync->{f1f2h}{ 'auto/auto' } = 'moto x 2' ; + is( 'moto x 2', imap2_folder_name( $mysync, 'auto/auto' ), 'imap2_folder_name: auto/auto' ) ; + + @{ $mysync->{ regextrans2 } } = ( 's,/,X,g' ) ; + is( q{INBOX}, imap2_folder_name( $mysync, q{} ), 'imap2_folder_name: empty string [s,/,X,g]' ) ; + is( 'blabla', imap2_folder_name( $mysync, 'blabla' ), 'imap2_folder_name: blabla [s,/,X,g]' ) ; + is('spam.spam', imap2_folder_name( $mysync, 'spam/spam'), 'imap2_folder_name: spam/spam [s,/,X,g]'); + is('spamXspam', imap2_folder_name( $mysync, 'spam.spam'), 'imap2_folder_name: spam.spam [s,/,X,g]'); + is('spam.spamXspam', imap2_folder_name( $mysync, 'spam/spam.spam'), 'imap2_folder_name: spam/spam.spam [s,/,X,g]'); + + @{ $mysync->{ regextrans2 } } = ( 's, ,_,g' ) ; + is('blabla', imap2_folder_name( $mysync, 'blabla'), 'imap2_folder_name: blabla [s, ,_,g]'); + is('bla_bla', imap2_folder_name( $mysync, 'bla bla'), 'imap2_folder_name: blabla [s, ,_,g]'); + + @{ $mysync->{ regextrans2 } } = ( q{s,(.*),\U$1,} ) ; + is( 'BLABLA', imap2_folder_name( $mysync, 'blabla' ), q{imap2_folder_name: blabla [s,\U(.*)\E,$1,]} ) ; + + $mysync->{ fixslash2 } = 1 ; + @{ $mysync->{ regextrans2 } } = ( ) ; + is(q{INBOX}, imap2_folder_name( $mysync, q{}), 'imap2_folder_name: empty string'); + is('blabla', imap2_folder_name( $mysync, 'blabla'), 'imap2_folder_name: blabla'); + is('spam.spam', imap2_folder_name( $mysync, 'spam/spam'), 'imap2_folder_name: spam/spam -> spam.spam'); + is('spam_spam', imap2_folder_name( $mysync, 'spam.spam'), 'imap2_folder_name: spam.spam -> spam_spam'); + is('spam.spam_spam', imap2_folder_name( $mysync, 'spam/spam.spam'), 'imap2_folder_name: spam/spam.spam -> spam.spam_spam'); + is('s pam.spam_spa m', imap2_folder_name( $mysync, 's pam/spam.spa m'), 'imap2_folder_name: s pam/spam.spa m -> s pam.spam_spa m'); + + $mysync->{ h1_sep } = '.'; + $mysync->{ h2_sep } = '/'; + is( q{INBOX}, imap2_folder_name( $mysync, q{}), 'imap2_folder_name: empty string'); + is('blabla', imap2_folder_name( $mysync, 'blabla'), 'imap2_folder_name: blabla'); + is('spam.spam', imap2_folder_name( $mysync, 'spam/spam'), 'imap2_folder_name: spam/spam -> spam.spam'); + is('spam/spam', imap2_folder_name( $mysync, 'spam.spam'), 'imap2_folder_name: spam.spam -> spam/spam'); + is('spam.spam/spam', imap2_folder_name( $mysync, 'spam/spam.spam'), 'imap2_folder_name: spam/spam.spam -> spam.spam/spam'); + + + + $mysync->{ fixslash2 } = 0 ; + $mysync->{ h1_prefix } = q{ }; + + is( 'spam.spam/spam', imap2_folder_name( $mysync, 'spam/spam.spam' ), 'imap2_folder_name: spam/spam.spam -> spam.spam/spam' ) ; + is( 'spam.spam/spam', imap2_folder_name( $mysync, ' spam/spam.spam' ), 'imap2_folder_name: spam/spam.spam -> spam.spam/spam' ) ; + + $mysync->{ h1_sep } = '.' ; + $mysync->{ h2_sep } = '/' ; + $mysync->{ h1_prefix } = 'INBOX.' ; + $mysync->{ h2_prefix } = q{} ; + @{ $mysync->{ regextrans2 } } = ( q{s,(.*),\U$1,} ) ; + is( 'BLABLA', imap2_folder_name( $mysync, 'blabla' ), 'imap2_folder_name: blabla' ) ; + is( 'TEST/TEST/TEST/TEST', imap2_folder_name( $mysync, 'INBOX.TEST.test.Test.tesT' ), 'imap2_folder_name: INBOX.TEST.test.Test.tesT' ) ; + @{ $mysync->{ regextrans2 } } = ( q{s,(.*),\L$1,} ) ; + is( 'test/test/test/test', imap2_folder_name( $mysync, 'INBOX.TEST.test.Test.tesT' ), 'imap2_folder_name: INBOX.TEST.test.Test.tesT' ) ; + + # INBOX + $mysync = {} ; + $mysync->{ h1_prefix } = q{Pf1.} ; + $mysync->{ h2_prefix } = q{Pf2/} ; + $mysync->{ h1_sep } = '.'; + $mysync->{ h2_sep } = '/'; + + # + #$mysync->{ debug } = 1 ; + is( 'Pf2/F1/F2/F3', imap2_folder_name( $mysync, 'F1.F2.F3' ), 'imap2_folder_name: F1.F2.F3 -> Pf2/F1/F2/F3' ) ; + is( 'Pf2/F1/INBOX', imap2_folder_name( $mysync, 'F1.INBOX' ), 'imap2_folder_name: F1.INBOX -> Pf2/F1/INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'INBOX' ), 'imap2_folder_name: INBOX -> INBOX' ) ; + + is( 'Pf2/F1/F2/F3', imap2_folder_name( $mysync, 'Pf1.F1.F2.F3' ), 'imap2_folder_name: Pf1.F1.F2.F3 -> Pf2/F1/F2/F3' ) ; + is( 'Pf2/F1/INBOX', imap2_folder_name( $mysync, 'Pf1.F1.INBOX' ), 'imap2_folder_name: Pf1.F1.INBOX -> Pf2/F1/INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'Pf1.INBOX' ), 'imap2_folder_name: Pf1.INBOX -> INBOX' ) ; # not Pf2/INBOX: Yes I can! + + + + # subfolder2 + $mysync = {} ; + $mysync->{ h1_prefix } = q{} ; + $mysync->{ h2_prefix } = q{} ; + $mysync->{ h1_sep } = '/'; + $mysync->{ h2_sep } = '.'; + + + set_regextrans2_for_subfolder2( $mysync ) ; + $mysync->{ subfolder2 } = 'S1.S2' ; + is( 'S1.S2.F1.F2.F3', imap2_folder_name( $mysync, 'F1/F2/F3' ), 'imap2_folder_name: F1/F2/F3 -> S1.S2.F1.F2.F3' ) ; + is( 'S1.S2.INBOX', imap2_folder_name( $mysync, 'INBOX' ), 'imap2_folder_name: F1/F2/F3 -> S1.S2.INBOX' ) ; + + $mysync = {} ; + $mysync->{ h1_prefix } = q{Pf1/} ; + $mysync->{ h2_prefix } = q{Pf2.} ; + $mysync->{ h1_sep } = '/'; + $mysync->{ h2_sep } = '.'; + #$mysync->{ debug } = 1 ; + + set_regextrans2_for_subfolder2( $mysync ) ; + $mysync->{ subfolder2 } = 'Pf2.S1.S2' ; + is( 'Pf2.S1.S2.F1.F2.F3', imap2_folder_name( $mysync, 'F1/F2/F3' ), 'imap2_folder_name: F1/F2/F3 -> Pf2.S1.S2.F1.F2.F3' ) ; + is( 'Pf2.S1.S2.INBOX', imap2_folder_name( $mysync, 'INBOX' ), 'imap2_folder_name: INBOX -> Pf2.S1.S2.INBOX' ) ; + is( 'Pf2.S1.S2.F1.F2.F3', imap2_folder_name( $mysync, 'Pf1/F1/F2/F3' ), 'imap2_folder_name: F1/F2/F3 -> Pf2.S1.S2.F1.F2.F3' ) ; + is( 'Pf2.S1.S2.INBOX', imap2_folder_name( $mysync, 'Pf1/INBOX' ), 'imap2_folder_name: INBOX -> Pf2.S1.S2.INBOX' ) ; + + # subfolder1 + # scenario as the reverse of the previous tests, separators point of vue + $mysync = {} ; + $mysync->{ h1_prefix } = q{Pf1.} ; + $mysync->{ h2_prefix } = q{Pf2/} ; + $mysync->{ h1_sep } = '.'; + $mysync->{ h2_sep } = '/'; + #$mysync->{ debug } = 1 ; + + $mysync->{ subfolder1 } = 'S1.S2' ; + is( 'Pf2/F1/F2/F3', imap2_folder_name( $mysync, 'S1.S2.F1.F2.F3' ), 'imap2_folder_name: S1.S2.F1.F2.F3 -> Pf2/F1/F2/F3' ) ; + is( 'Pf2/F1/F2/F3', imap2_folder_name( $mysync, 'Pf1.S1.S2.F1.F2.F3' ), 'imap2_folder_name: Pf1.S1.S2.F1.F2.F3 -> Pf2/F1/F2/F3' ) ; + + is( 'INBOX', imap2_folder_name( $mysync, 'S1.S2.INBOX' ), 'imap2_folder_name: S1.S2.INBOX -> INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'S1.S2' ), 'imap2_folder_name: S1.S2 -> INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'S1.S2.' ), 'imap2_folder_name: S1.S2. -> INBOX' ) ; + + is( 'INBOX', imap2_folder_name( $mysync, 'Pf1.S1.S2.INBOX' ), 'imap2_folder_name: Pf1.S1.S2.INBOX -> INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'Pf1.S1.S2' ), 'imap2_folder_name: Pf1.S1.S2 -> INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'Pf1.S1.S2.' ), 'imap2_folder_name: Pf1.S1.S2. -> INBOX' ) ; + + + $mysync->{ subfolder1 } = 'S1.S2.' ; + is( 'Pf2/F1/F2/F3', imap2_folder_name( $mysync, 'S1.S2.F1.F2.F3' ), 'imap2_folder_name: S1.S2.F1.F2.F3 -> Pf2/F1/F2/F3' ) ; + is( 'Pf2/F1/F2/F3', imap2_folder_name( $mysync, 'Pf1.S1.S2.F1.F2.F3' ), 'imap2_folder_name: Pf1.S1.S2.F1.F2.F3 -> Pf2/F1/F2/F3' ) ; + + is( 'INBOX', imap2_folder_name( $mysync, 'S1.S2.INBOX' ), 'imap2_folder_name: S1.S2.INBOX -> INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'S1.S2' ), 'imap2_folder_name: S1.S2 -> INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'S1.S2.' ), 'imap2_folder_name: S1.S2. -> INBOX' ) ; + + is( 'INBOX', imap2_folder_name( $mysync, 'Pf1.S1.S2.INBOX' ), 'imap2_folder_name: Pf1.S1.S2.INBOX -> INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'Pf1.S1.S2' ), 'imap2_folder_name: Pf1.S1.S2 -> INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'Pf1.S1.S2.' ), 'imap2_folder_name: Pf1.S1.S2. -> INBOX' ) ; + + + # subfolder1 + # scenario as Gmail + $mysync = {} ; + $mysync->{ h1_prefix } = q{} ; + $mysync->{ h2_prefix } = q{} ; + $mysync->{ h1_sep } = '/'; + $mysync->{ h2_sep } = '/'; + #$mysync->{ debug } = 1 ; + + $mysync->{ subfolder1 } = 'S1/S2' ; + is( 'F1/F2/F3', imap2_folder_name( $mysync, 'S1/S2/F1/F2/F3' ), 'imap2_folder_name: S1/S2/F1/F2/F3 -> F1/F2/F3' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'S1/S2/INBOX' ), 'imap2_folder_name: S1/S2/INBOX -> INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'S1/S2' ), 'imap2_folder_name: S1/S2 -> INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'S1/S2/' ), 'imap2_folder_name: S1/S2/ -> INBOX' ) ; + + $mysync->{ subfolder1 } = 'S1/S2/' ; + is( 'F1/F2/F3', imap2_folder_name( $mysync, 'S1/S2/F1/F2/F3' ), 'imap2_folder_name: S1/S2/F1/F2/F3 -> F1/F2/F3' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'S1/S2/INBOX' ), 'imap2_folder_name: S1/S2/INBOX -> INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'S1/S2' ), 'imap2_folder_name: S1/S2 -> INBOX' ) ; + is( 'INBOX', imap2_folder_name( $mysync, 'S1/S2/' ), 'imap2_folder_name: S1/S2/ -> INBOX' ) ; + + + note( 'Leaving tests_imap2_folder_name()' ) ; + return ; +} + + +# Global variables to remove: +# None? + + +sub imap2_folder_name +{ + my $mysync = shift ; + my ( $h1_fold ) = shift ; + my ( $h2_fold ) ; + if ( $mysync->{f1f2h}{ $h1_fold } ) { + $h2_fold = $mysync->{f1f2h}{ $h1_fold } ; + ( $mysync->{ debug } or $mysync->{debugfolders} ) and myprint( "f1f2 [$h1_fold] -> [$h2_fold]\n" ) ; + return( $h2_fold ) ; + } + if ( $mysync->{f1f2auto}{ $h1_fold } ) { + $h2_fold = $mysync->{f1f2auto}{ $h1_fold } ; + ( $mysync->{ debug } or $mysync->{debugfolders} ) and myprint( "automap [$h1_fold] -> [$h2_fold]\n" ) ; + return( $h2_fold ) ; + } + + if ( $mysync->{ subfolder1 } ) + { + my $esc_h1_sep = "\\" . $mysync->{ h1_sep } ; + # case where subfolder1 has the sep1 at the end, then remove it + my $part_to_removed = remove_last_char_if_is( $mysync->{ subfolder1 }, $mysync->{ h1_sep } ) ; + # remove the subfolder1 part and the sep1 if present after + $h1_fold =~ s{$part_to_removed($esc_h1_sep)?}{} ; + #myprint( "h1_fold=$h1_fold\n" ) ; + } + + if ( ( q{} eq $h1_fold ) or ( $mysync->{ h1_prefix } eq $h1_fold ) ) + { + $h1_fold = 'INBOX' ; + } + + $h2_fold = prefix_seperator_invertion( $mysync, $h1_fold ) ; + $h2_fold = regextrans2( $mysync, $h2_fold ) ; + return( $h2_fold ) ; +} + + +sub tests_remove_last_char_if_is +{ + note( 'Entering tests_remove_last_char_if_is()' ) ; + + is( undef, remove_last_char_if_is( ), 'remove_last_char_if_is: no args => undef' ) ; + is( q{}, remove_last_char_if_is( q{} ), 'remove_last_char_if_is: empty => empty' ) ; + is( q{}, remove_last_char_if_is( q{}, 'Z' ), 'remove_last_char_if_is: empty Z => empty' ) ; + is( q{}, remove_last_char_if_is( 'Z', 'Z' ), 'remove_last_char_if_is: Z Z => empty' ) ; + is( 'abc', remove_last_char_if_is( 'abcZ', 'Z' ), 'remove_last_char_if_is: abcZ Z => abc' ) ; + is( 'abcY', remove_last_char_if_is( 'abcY', 'Z' ), 'remove_last_char_if_is: abcY Z => abcY' ) ; + note( 'Leaving tests_remove_last_char_if_is()' ) ; + return ; +} + + + + +sub remove_last_char_if_is +{ + my $string = shift ; + my $char = shift ; + + if ( ! defined $string ) + { + return ; + } + + if ( ! defined $char ) + { + return $string ; + } + + my $last_char = substr $string, -1 ; + if ( $char eq $last_char ) + { + chop $string ; + return $string ; + } + else + { + return $string ; + } +} + +sub tests_prefix_seperator_invertion +{ + note( 'Entering tests_prefix_seperator_invertion()' ) ; + + is( undef, prefix_seperator_invertion( ), 'prefix_seperator_invertion: no args => undef' ) ; + is( q{}, prefix_seperator_invertion( undef, q{} ), 'prefix_seperator_invertion: empty string => empty string' ) ; + is( 'lalala', prefix_seperator_invertion( undef, 'lalala' ), 'prefix_seperator_invertion: lalala => lalala' ) ; + is( 'lal/ala', prefix_seperator_invertion( undef, 'lal/ala' ), 'prefix_seperator_invertion: lal/ala => lal/ala' ) ; + is( 'lal.ala', prefix_seperator_invertion( undef, 'lal.ala' ), 'prefix_seperator_invertion: lal.ala => lal.ala' ) ; + is( '////', prefix_seperator_invertion( undef, '////' ), 'prefix_seperator_invertion: //// => ////' ) ; + is( '.....', prefix_seperator_invertion( undef, '.....' ), 'prefix_seperator_invertion: ..... => .....' ) ; + + my $mysync = { + h1_prefix => q{}, + h2_prefix => q{}, + h1_sep => '/', + h2_sep => '/', + } ; + + is( q{}, prefix_seperator_invertion( $mysync, q{} ), 'prefix_seperator_invertion: $mysync empty string => empty string' ) ; + is( 'lalala', prefix_seperator_invertion( $mysync, 'lalala' ), 'prefix_seperator_invertion: $mysync lalala => lalala' ) ; + is( 'lal/ala', prefix_seperator_invertion( $mysync, 'lal/ala' ), 'prefix_seperator_invertion: $mysync lal/ala => lal/ala' ) ; + is( 'lal.ala', prefix_seperator_invertion( $mysync, 'lal.ala' ), 'prefix_seperator_invertion: $mysync lal.ala => lal.ala' ) ; + is( '////', prefix_seperator_invertion( $mysync, '////' ), 'prefix_seperator_invertion: $mysync //// => ////' ) ; + is( '.....', prefix_seperator_invertion( $mysync, '.....' ), 'prefix_seperator_invertion: $mysync ..... => .....' ) ; + + $mysync = { + h1_prefix => 'PPP', + h2_prefix => 'QQQ', + h1_sep => 's', + h2_sep => 't', + } ; + + is( q{QQQ}, prefix_seperator_invertion( $mysync, q{} ), 'prefix_seperator_invertion: PPPQQQst empty string => QQQ' ) ; + is( 'QQQlalala', prefix_seperator_invertion( $mysync, 'lalala' ), 'prefix_seperator_invertion: PPPQQQst lalala => QQQlalala' ) ; + is( 'QQQlal/ala', prefix_seperator_invertion( $mysync, 'lal/ala' ), 'prefix_seperator_invertion: PPPQQQst lal/ala => QQQlal/ala' ) ; + is( 'QQQlal.ala', prefix_seperator_invertion( $mysync, 'lal.ala' ), 'prefix_seperator_invertion: PPPQQQst lal.ala => QQQlal.ala' ) ; + is( 'QQQ////', prefix_seperator_invertion( $mysync, '////' ), 'prefix_seperator_invertion: PPPQQQst //// => QQQ////' ) ; + is( 'QQQ.....', prefix_seperator_invertion( $mysync, '.....' ), 'prefix_seperator_invertion: PPPQQQst ..... => QQQ.....' ) ; + + is( 'QQQPlalala', prefix_seperator_invertion( $mysync, 'PPPPlalala' ), 'prefix_seperator_invertion: PPPQQQst PPPPlalala => QQQPlalala' ) ; + is( 'QQQ', prefix_seperator_invertion( $mysync, 'PPP' ), 'prefix_seperator_invertion: PPPQQQst PPP => QQQ' ) ; + is( 'QQQttt', prefix_seperator_invertion( $mysync, 'sss' ), 'prefix_seperator_invertion: PPPQQQst sss => QQQttt' ) ; + is( 'QQQt', prefix_seperator_invertion( $mysync, 's' ), 'prefix_seperator_invertion: PPPQQQst s => QQQt' ) ; + is( 'QQQtAAAtBBB', prefix_seperator_invertion( $mysync, 'PPPsAAAsBBB' ), 'prefix_seperator_invertion: PPPQQQst PPPsAAAsBBB => QQQtAAAtBBB' ) ; + + note( 'Leaving tests_prefix_seperator_invertion()' ) ; + return ; +} + +# Global variables to remove: + + +sub prefix_seperator_invertion +{ + my $mysync = shift ; + my $h1_fold = shift ; + my $h2_fold ; + + if ( not defined $h1_fold ) { return ; } + + my $my_h1_prefix = $mysync->{ h1_prefix } || q{} ; + my $my_h2_prefix = $mysync->{ h2_prefix } || q{} ; + my $my_h1_sep = $mysync->{ h1_sep } || '/' ; + my $my_h2_sep = $mysync->{ h2_sep } || '/' ; + + # first we remove the prefix + $h1_fold =~ s/^\Q$my_h1_prefix\E//x ; + ( $mysync->{ debug } or $mysync->{debugfolders} ) and myprint( "removed host1 prefix: [$h1_fold]\n" ) ; + $h2_fold = separator_invert( $mysync, $h1_fold, $my_h1_sep, $my_h2_sep ) ; + ( $mysync->{ debug } or $mysync->{debugfolders} ) and myprint( "inverted separators: [$h2_fold]\n" ) ; + + # Adding the prefix supplied by namespace or the --prefix2 option + # except for INBOX or Inbox + if ( $h2_fold !~ m/^INBOX$/xi ) + { + $h2_fold = $my_h2_prefix . $h2_fold ; + } + + ( $mysync->{ debug } or $mysync->{debugfolders} ) and myprint( "added host2 prefix: [$h2_fold]\n" ) ; + return( $h2_fold ) ; +} + +sub tests_separator_invert +{ + note( 'Entering tests_separator_invert()' ) ; + + my $mysync = {} ; + $mysync->{ fixslash2 } = 0 ; + ok( not( defined separator_invert( ) ), 'separator_invert: no args' ) ; + ok( not( defined separator_invert( q{} ) ), 'separator_invert: not enough args' ) ; + ok( not( defined separator_invert( q{}, q{} ) ), 'separator_invert: not enough args' ) ; + + ok( q{} eq separator_invert( $mysync, q{}, q{}, q{} ), 'separator_invert: 3 empty strings' ) ; + ok( 'lalala' eq separator_invert( $mysync, 'lalala', q{}, q{} ), 'separator_invert: empty separator' ) ; + ok( 'lalala' eq separator_invert( $mysync, 'lalala', '/', '/' ), 'separator_invert: same separator /' ) ; + ok( 'lal/ala' eq separator_invert( $mysync, 'lal/ala', '/', '/' ), 'separator_invert: same separator / 2' ) ; + ok( 'lal.ala' eq separator_invert( $mysync, 'lal/ala', '/', '.' ), 'separator_invert: separators /.' ) ; + ok( 'lal/ala' eq separator_invert( $mysync, 'lal.ala', '.', '/' ), 'separator_invert: separators ./' ) ; + ok( 'la.l/ala' eq separator_invert( $mysync, 'la/l.ala', '.', '/' ), 'separator_invert: separators ./' ) ; + + ok( 'l/al.ala' eq separator_invert( $mysync, 'l.al/ala', '/', '.' ), 'separator_invert: separators /.' ) ; + $mysync->{ fixslash2 } = 1 ; + ok( 'l_al.ala' eq separator_invert( $mysync, 'l.al/ala', '/', '.' ), 'separator_invert: separators /.' ) ; + + note( 'Leaving tests_separator_invert()' ) ; + return ; +} + +# Global variables to remove: +# +sub separator_invert +{ + my( $mysync, $h1_fold, $h1_separator, $h2_separator ) = @_ ; + + return( undef ) if ( not all_defined( $mysync, $h1_fold, $h1_separator, $h2_separator ) ) ; + # The separator we hope we'll never encounter: 00000000 == 0x00 + my $o_sep = "\000" ; + + my $h2_fold = $h1_fold ; + $h2_fold =~ s,\Q$h2_separator,$o_sep,xg ; + $h2_fold =~ s,\Q$h1_separator,$h2_separator,xg ; + $h2_fold =~ s,\Q$o_sep,$h1_separator,xg ; + $h2_fold =~ s,/,_,xg if( $mysync->{ fixslash2 } and '/' ne $h2_separator and '/' eq $h1_separator ) ; + return( $h2_fold ) ; +} + + +sub regextrans2 +{ + my( $mysync, $h2_fold ) = @_ ; + # Transforming the folder name by the --regextrans2 option(s) + foreach my $regextrans2 ( @{ $mysync->{ regextrans2 } } ) { + my $h2_fold_before = $h2_fold ; + my $ret = eval "\$h2_fold =~ $regextrans2 ; 1 " ; + ( $mysync->{ debug } or $mysync->{debugfolders} ) and myprint( "[$h2_fold_before] -> [$h2_fold] using regextrans2 [$regextrans2]\n" ) ; + if ( not ( defined $ret ) or $EVAL_ERROR ) { + $mysync->{nb_errors}++ ; + exit_clean( $mysync, $EX_USAGE, + "error: eval regextrans2 '$regextrans2': $EVAL_ERROR\n" + ) ; + } + } + return( $h2_fold ) ; +} + + +sub tests_decompose_regex +{ + note( 'Entering tests_decompose_regex()' ) ; + + ok( 1, 'decompose_regex 1' ) ; + ok( 0 == compare_lists( [ q{}, q{} ], [ decompose_regex( q{} ) ] ), 'decompose_regex empty string' ) ; + ok( 0 == compare_lists( [ '.*', 'lala' ], [ decompose_regex( 's/.*/lala/' ) ] ), 'decompose_regex s/.*/lala/' ) ; + + note( 'Leaving tests_decompose_regex()' ) ; + return ; +} + +sub decompose_regex +{ + my $regex = shift ; + my( $left_part, $right_part ) ; + + ( $left_part, $right_part ) = $regex =~ m{^s/((?:[^/]|\\/)+)/((?:[^/]|\\/)+)/}x; + return( q{}, q{} ) if not $left_part ; + return( $left_part, $right_part ) ; +} + + + +sub tests_timenext +{ + note( 'Entering tests_timenext()' ) ; + + is( undef, timenext( ), 'timenext: no args => undef' ) ; + my $mysync ; + is( undef, timenext( $mysync ), 'timenext: undef => undef' ) ; + $mysync = {} ; + ok( time - timenext( $mysync ) <= 1e-02, 'timenext: defined first time => ~ time' ) ; + ok( timenext( $mysync ) <= 1e-02, 'timenext: second time => less than 1e-02' ) ; + ok( timenext( $mysync ) <= 1e-02, 'timenext: third time => less than 1e-02' ) ; + + note( 'Leaving tests_timenext()' ) ; + return ; +} + + +sub timenext +{ + my $mysync = shift ; + + if ( ! defined $mysync ) + { + return ; + } + my ( $timenow, $timediff ) ; + + $mysync->{ timebefore } ||= 0; # epoch... + $timenow = time ; + $timediff = $timenow - $mysync->{ timebefore } ; + $mysync->{ timebefore } = $timenow ; + # myprint( "timenext: $timediff\n" ) ; + return( $timediff ) ; +} + + +sub tests_timesince +{ + note( 'Entering tests_timesince()' ) ; + + ok( timesince( time - 1 ) - 1 <= 1e-02, 'timesince: time - 1 => <= 1 + 1e-02' ) ; + ok( timesince( time ) <= 1e-02, 'timesince: time => <= 1e-02' ) ; + ok( timesince( ) - time <= 1e-02, 'timesince: no args => <= time + 1e-02' ) ; + note( 'Leaving tests_timesince()' ) ; + return ; +} + + + +sub timesince +{ + my $timeinit = shift || 0 ; + my ( $timenow, $timediff ) ; + $timenow = time ; + $timediff = $timenow - $timeinit ; + # Often used in a division so no 0 but a nano seconde. + return( max( $timediff, min( 1e-09, $timediff ) ) ) ; +} + + + + +sub tests_regexflags +{ + note( 'Entering tests_regexflags()' ) ; + + my $mysync = {} ; + + ok( q{} eq regexflags( $mysync, q{} ), 'regexflags, null string q{}' ) ; + ok( q{\Seen NonJunk $Spam} eq regexflags( $mysync, q{\Seen NonJunk $Spam} ), q{regexflags, nothing to do} ) ; + + @{ $mysync->{ regexflag } } = ('I am BAD' ) ; + ok( not ( defined regexflags( $mysync, q{} ) ), 'regexflags, bad regex' ) ; + + @{ $mysync->{ regexflag } } = ( 's/NonJunk//g' ) ; + ok( q{\Seen $Spam} eq regexflags( $mysync, q{\Seen NonJunk $Spam} ), q{regexflags, remove NonJunk: 's/NonJunk//g'} ) ; + @{ $mysync->{ regexflag } } = ( q{s/\$Spam//g} ) ; + ok( q{\Seen NonJunk } eq regexflags( $mysync, q{\Seen NonJunk $Spam} ), q{regexflags, remove $Spam: 's/\$Spam//g'} ) ; + + @{ $mysync->{ regexflag } } = ( 's/\\\\Seen//g' ) ; + + ok( q{ NonJunk $Spam} eq regexflags( $mysync, q{\Seen NonJunk $Spam} ), q{regexflags, remove \Seen: 's/\\\\\\\\Seen//g'} ) ; + + @{ $mysync->{ regexflag } } = ( 's/(\s|^)[^\\\\]\w+//g' ) ; + ok( q{\Seen \Middle \End} eq regexflags( $mysync, q{\Seen NonJunk \Middle $Spam \End} ), q{regexflags: only \word among \Seen NonJunk \Middle $Spam \End} ) ; + ok( q{ \Seen \Middle \End1} eq regexflags( $mysync, q{Begin \Seen NonJunk \Middle $Spam \End1 End} ), + q{regexflags: only \word among Begin \Seen NonJunk \Middle $Spam \End1 End} ) ; + + @{ $mysync->{ regexflag } } = ( q{s/.*?(Keep1|Keep2|Keep3)/$1 /g} ) ; + ok( 'Keep1 Keep2 ReB' eq regexflags( $mysync, 'ReA Keep1 REM Keep2 ReB' ), 'Keep only regex' ) ; + + ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'REM REM Keep1 Keep2' ), 'Keep only regex' ) ; + ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'Keep1 REM REM Keep2' ), 'Keep only regex' ) ; + ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'REM Keep1 REM REM Keep2' ), 'Keep only regex' ) ; + ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'Keep1 Keep2' ), 'Keep only regex' ) ; + ok( 'Keep1 ' eq regexflags( $mysync, 'REM Keep1' ), 'Keep only regex' ) ; + + @{ $mysync->{ regexflag } } = ( q{s/(Keep1|Keep2|Keep3) (?!(Keep1|Keep2|Keep3)).*/$1 /g} ) ; + ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'Keep1 Keep2 ReB' ), 'Keep only regex' ) ; + ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'Keep1 Keep2 REM REM REM' ), 'Keep only regex' ) ; + ok( 'Keep2 ' eq regexflags( $mysync, 'Keep2 REM REM REM' ), 'Keep only regex' ) ; + + + @{ $mysync->{ regexflag } } = ( q{s/.*?(Keep1|Keep2|Keep3)/$1 /g}, + 's/(Keep1|Keep2|Keep3) (?!(Keep1|Keep2|Keep3)).*/$1 /g' ) ; + ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'REM Keep1 REM Keep2 REM' ), 'Keep only regex' ) ; + ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'Keep1 REM Keep2 REM' ), 'Keep only regex' ) ; + ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'REM Keep1 Keep2 REM' ), 'Keep only regex' ) ; + ok( 'Keep1 Keep2 ' eq regexflags( $mysync, 'REM Keep1 REM Keep2' ), 'Keep only regex' ) ; + ok( 'Keep1 Keep2 Keep3 ' eq regexflags( $mysync, 'REM Keep1 REM Keep2 REM REM Keep3 REM' ), 'Keep only regex' ) ; + ok( 'Keep1 ' eq regexflags( $mysync, 'REM REM Keep1 REM REM REM ' ), 'Keep only regex' ) ; + ok( 'Keep1 Keep3 ' eq regexflags( $mysync, 'RE1 Keep1 RE2 Keep3 RE3 RE4 RE5 ' ), 'Keep only regex' ) ; + + @{ $mysync->{ regexflag } } = ( 's/(.*)/$1 jrdH8u/' ) ; + ok('REM REM REM REM REM jrdH8u' eq regexflags( $mysync, 'REM REM REM REM REM' ), q{Add jrdH8u 's/(.*)/\$1 jrdH8u/'} ) ; + @{ $mysync->{ regexflag } } = ('s/jrdH8u *//' ); + ok('REM REM REM REM REM ' eq regexflags( $mysync, 'REM REM REM REM REM jrdH8u' ), q{Remove jrdH8u s/jrdH8u *//} ) ; + + @{ $mysync->{ regexflag } } = ( + 's/.*?(?:(\\\\(?:Answered|Flagged|Deleted|Seen|Draft)\s?)|$)/defined($1)?$1:q()/eg' + ); + + ok( '\\Deleted \\Answered ' + eq regexflags( $mysync, 'Blabla \$Junk \\Deleted machin \\Answered truc' ), + 'Keep only regex: Exchange case (Phil)' ) ; + + ok( q{} eq regexflags( $mysync, q{} ), 'Keep only regex: Exchange case, null string (Phil)' ) ; + + ok( q{} + eq regexflags( $mysync, 'Blabla $Junk machin truc' ), + 'Keep only regex: Exchange case, no accepted flags (Phil)' ) ; + + ok('\\Deleted \\Answered \\Draft \\Flagged ' + eq regexflags( $mysync, '\\Deleted \\Answered \\Draft \\Flagged ' ), + 'Keep only regex: Exchange case (Phil)' ) ; + + @{ $mysync->{ regexflag } } = ( 's/\\\\Flagged//g' ) ; + + is('\Deleted \Answered \Draft ', + regexflags( $mysync, '\\Deleted \\Answered \\Draft \\Flagged ' ), + 'regexflags: remove \Flagged 1' ) ; + + is('\\Deleted \\Answered \\Draft', + regexflags( $mysync, '\\Deleted \\Flagged \\Answered \\Draft' ), + 'regexflags: remove \Flagged 2' ) ; + + # I didn't understand why it gives \F + # https://perldoc.perl.org/perlrebackslash.html + # \F Foldcase till \E. Not in []. + # https://perldoc.perl.org/functions/fc.html + + # \F Not available in old Perl so I comment the test + + # @{ $mysync->{ regexflag } } = ( 's/\\Flagged/X/g' ) ; + #is('\Deleted FX \Answered \FX \Draft \FX', + #regexflags( '\Deleted Flagged \Answered \Flagged \Draft \Flagged' ), + # 'regexflags: remove \Flagged 3 mistery...' ) ; + + $mysync->{ regexflag } = [ ] ; + $mysync->{ filterbuggyflags } = 1 ; + filterbuggyflags( $mysync ) ; + + is( '\Deleted \Answered \Draft \Flagged', + regexflags( $mysync, '\\Deleted \\Answered \\RECEIPTCHECKED \\Draft \\Indexed \\Flagged' ), + 'regexflags: remove famous /X 1' ) ; + + is( '\\Deleted \\Flagged \\Answered \\Draft', + regexflags( $mysync, '\\Deleted \\RECEIPTCHECKED \\Flagged \\Answered \\Indexed \\Draft' ), + 'regexflags: remove famous /X 2' ) ; + + is( '\ ', '\\ ', 'regexflags: \ is \\ ' ) ; + is( '\\ ', '\\ ', 'regexflags: \\ is \\ ' ) ; + is( '\\ \ ', '\ \\ ', 'regexflags: \\ \ is \ \\ ' ) ; + note( 'Leaving tests_regexflags()' ) ; + return ; +} + +sub regexflags +{ + my $mysync = shift ; + my $flags = shift ; + + foreach my $regexflag ( @{ $mysync->{ regexflag } } ) + { + my $flags_orig = $flags ; + $debugflags and myprint( "eval \$flags =~ $regexflag\n" ) ; + my $ret = eval "\$flags =~ $regexflag ; 1 " ; + $debugflags and myprint( "regexflag $regexflag [$flags_orig] -> [$flags]\n" ) ; + if( not ( defined $ret ) or $EVAL_ERROR ) { + myprint( "Error: eval regexflag '$regexflag': $EVAL_ERROR\n" ) ; + return( undef ) ; + } + } + return( $flags ) ; +} + + +sub filterbuggyflags +{ + my $mysync = shift ; + if ( $mysync->{ filterbuggyflags } ) + { + unshift @{ $mysync->{ regexflag } }, buggyflagsregex( ) ; + } + return ; +} + + +sub tests_remove_doublequotes_if_any +{ + note( 'Entering tests_remove_doublequotes_if_any()' ) ; + # the number of tests is stupid here + is( undef, remove_doublequotes_if_any( ), 'remove_doublequotes_if_any: no args => undef' ) ; + is( q{}, remove_doublequotes_if_any( q{} ), 'remove_doublequotes_if_any: empty string => empty string' ) ; + is( q{}, remove_doublequotes_if_any( q{""} ), 'remove_doublequotes_if_any: double-quotes => empty string' ) ; + is( q{}, remove_doublequotes_if_any( q{"""} ), 'remove_doublequotes_if_any: double-quotes => empty string' ) ; + is( q{}, remove_doublequotes_if_any( q{"""} ), 'remove_doublequotes_if_any: double-quotes => empty string' ) ; + is( q{toto}, remove_doublequotes_if_any( q{"toto"} ), 'remove_doublequotes_if_any: "toto" => toto' ) ; + is( q{toto}, remove_doublequotes_if_any( q{toto} ), 'remove_doublequotes_if_any: toto => toto' ) ; + is( q{toto}, remove_doublequotes_if_any( q{to"to} ), 'remove_doublequotes_if_any: to"to => toto' ) ; + is( q{toto}, remove_doublequotes_if_any( q{toto"} ), 'remove_doublequotes_if_any: toto" => toto' ) ; + is( q{toto}, remove_doublequotes_if_any( q{"toto} ), 'remove_doublequotes_if_any: "toto => toto' ) ; + is( q{toto}, remove_doublequotes_if_any( q{"to"to} ), 'remove_doublequotes_if_any: "to"to => toto' ) ; + is( q{toto}, remove_doublequotes_if_any( q{to"to"} ), 'remove_doublequotes_if_any: to"to" => toto' ) ; + + is( q{toto}, remove_doublequotes_if_any( q{to\"to} ), 'remove_doublequotes_if_any: to\"to => toto' ) ; + is( q{toto}, remove_doublequotes_if_any( q{toto\"} ), 'remove_doublequotes_if_any: toto\" => toto' ) ; + is( q{toto}, remove_doublequotes_if_any( q{\"toto} ), 'remove_doublequotes_if_any: \"toto => toto' ) ; + is( q{toto}, remove_doublequotes_if_any( q{\"to\"to} ), 'remove_doublequotes_if_any: \"to\"to => toto' ) ; + is( q{toto}, remove_doublequotes_if_any( q{to\"to\"} ), 'remove_doublequotes_if_any: to\"to" => toto' ) ; + + + note( 'Leaving tests_remove_doublequotes_if_any()' ) ; + return ; +} + + + +sub remove_doublequotes_if_any +{ + my $string = shift ; + + if ( ! defined $string ) { return ; } + $string =~ s/\\\"//g ; + $string =~ tr/"//d ; + return $string ; +} + + +# No globals here +sub acls_sync +{ +# https://tools.ietf.org/html/rfc4314 +# Standard Rights: +# https://tools.ietf.org/html/rfc4314#section-2.1 + + my( $mysync, $h1_fold, $h2_fold ) = @_ ; + if ( $mysync->{ syncacls } ) { + my $h1_hash = $mysync->{imap1}->getacl($h1_fold) + or myprint( "Host1: Could not getacl for $h1_fold: $EVAL_ERROR\n" ) ; + my $h2_hash = $mysync->{imap2}->getacl($h2_fold) + or myprint( "Host2: Could not getacl for $h2_fold: $EVAL_ERROR\n" ) ; + + my %users = map { ($_, 1) } ( keys %{ $h1_hash} , keys %{ $h2_hash } ) ; + foreach my $user (sort keys %users ) { + my $h1_acl = remove_doublequotes_if_any( $h1_hash->{$user} ) || '' ; + my $h2_acl = remove_doublequotes_if_any( $h2_hash->{$user} ) || '' ; + myprint( "Host1: user $user has acl [$h1_acl] on host1\n" ) ; + myprint( "Host2: user $user has acl [$h2_acl] on host2\n" ) ; + # removes surrounding double-quotes if any + my $user_no_quotes = remove_doublequotes_if_any( $user ) ; + + if ( $h1_hash->{$user} + && $h2_hash->{$user} + && $h1_hash->{$user} eq $h2_hash->{$user} ) + { + myprint( "Host2: user $user_no_quotes has already the same acl, no need to set it.\n" ) ; + next ; + } + myprint( "Host2: setting acl for folder $h2_fold user $user_no_quotes acl $h1_acl $mysync->{dry_message}\n" ) ; + unless ( $mysync->{dry} ) { + $mysync->{imap2}->setacl( $h2_fold, $user_no_quotes, $h1_acl ) + or myprint( "Could not set acl for user $user_no_quotes on host2: $EVAL_ERROR\n" ) ; + } + } + } + return ; +} + + +sub tests_permanentflags +{ + note( 'Entering tests_permanentflags()' ) ; + + my $mysync = { } ; + ok( q{} eq permanentflags( $mysync, ' * OK [PERMANENTFLAGS (\* \Draft \Answered)] Limited' ), + 'permanentflags \*' ) ; + + ok( '\Draft \Answered' eq permanentflags( $mysync, ' * OK [PERMANENTFLAGS (\Draft \Answered)] Limited' ), + 'permanentflags \Draft \Answered' ) ; + + ok( '\Draft \Answered' + eq permanentflags( $mysync, 'Blabla', + ' * OK [PERMANENTFLAGS (\Draft \Answered)] Limited', + 'Blabla' ), + 'permanentflags \Draft \Answered' + ) ; + + ok( q{} eq permanentflags( $mysync, 'Blabla' ), 'permanentflags nothing' ) ; + + note( 'Leaving tests_permanentflags()' ) ; + return ; +} + +sub permanentflags +{ + my $mysync = shift ; + + my @lines = @_ ; + + foreach my $line (@lines) { + if ( $line =~ m{\[PERMANENTFLAGS\s\(([^)]+?)\)\]}x ) { + ( $debugflags or $sync->{ debug } ) and myprint( "permanentflags: $line" ) ; + my $permanentflags = $1 ; + if ( $permanentflags =~ m{\\\*}x ) + { + $permanentflags = q{} ; + } + return( $permanentflags ) ; + } ; + } + return( q{} ) ; +} + +sub tests_flags_filter +{ + note( 'Entering tests_flags_filter()' ) ; + + ok( '\Seen' eq flags_filter('\Seen', '\Draft \Seen \Answered'), 'flags_filter ' ); + ok( q{} eq flags_filter('\Seen', '\Draft \Answered'), 'flags_filter ' ); + ok( '\Seen' eq flags_filter('\Seen', '\Seen'), 'flags_filter ' ); + ok( '\Seen' eq flags_filter('\Seen', ' \Seen '), 'flags_filter ' ); + ok( '\Seen \Draft' + eq flags_filter('\Seen \Draft', '\Draft \Seen \Answered'), 'flags_filter ' ); + ok( '\Seen \Draft' + eq flags_filter('\Seen \Draft', ' \Draft \Seen \Answered '), 'flags_filter ' ); + + note( 'Leaving tests_flags_filter()' ) ; + return ; +} + +sub flags_filter +{ + my( $flags, $allowed_flags ) = @_ ; + + my @flags = split /\s+/x, $flags ; + my %allowed_flags = map { $_ => 1 } split q{ }, $allowed_flags ; + my @flags_out = map { exists $allowed_flags{$_} ? $_ : () } @flags ; + + my $flags_out = join q{ }, @flags_out ; + + return( $flags_out ) ; +} + +sub tests_flagscase +{ + note( 'Entering tests_flagscase()' ) ; + + ok( '\Seen' eq flagscase( '\Seen' ), 'flagscase: \Seen -> \Seen' ) ; + ok( '\Seen' eq flagscase( '\SEEN' ), 'flagscase: \SEEN -> \Seen' ) ; + + ok( '\Seen \Draft' eq flagscase( '\SEEN \DRAFT' ), 'flagscase: \SEEN \DRAFT -> \Seen \Draft' ) ; + ok( '\Draft \Seen' eq flagscase( '\DRAFT \SEEN' ), 'flagscase: \DRAFT \SEEN -> \Draft \Seen' ) ; + + ok( '\Draft LALA \Seen' eq flagscase( '\DRAFT LALA \SEEN' ), 'flagscase: \DRAFT LALA \SEEN -> \Draft LALA \Seen' ) ; + ok( '\Draft lala \Seen' eq flagscase( '\DRAFT lala \SEEN' ), 'flagscase: \DRAFT lala \SEEN -> \Draft lala \Seen' ) ; + + note( 'Leaving tests_flagscase()' ) ; + return ; +} + +sub flagscase +{ + my $flags = shift ; + + my @flags = split /\s+/x, $flags ; + my %rfc_flags = map { $_ => 1 } split q{ }, '\Answered \Flagged \Deleted \Seen \Draft' ; + my @flags_out = map { exists $rfc_flags{ ucsecond( lc $_ ) } ? ucsecond( lc $_ ) : $_ } @flags ; + + my $flags_out = join q{ }, @flags_out ; + + return( $flags_out ) ; +} + + + +sub tests_flags_for_host2 +{ + note( 'Entering tests_flags_for_host2()' ) ; + + is( undef, flags_for_host2( ), 'flags_for_host2: no args => undef' ) ; + + my $mysync ; + is( undef, flags_for_host2( $mysync ), 'flags_for_host2: undef => undef' ) ; + + $mysync = { } ; + is( undef, flags_for_host2( $mysync ), 'flags_for_host2: nothing => undef' ) ; + + is( q{}, flags_for_host2( $mysync, '' ), 'flags_for_host2: no flags => empty string' ) ; + + is( q{}, flags_for_host2( $mysync, '\Recent' ), 'flags_for_host2: \Recent => empty string' ) ; + + is( q{\Seen}, flags_for_host2( $mysync, '\Recent \Seen' ), 'flags_for_host2: \Recent \Seen => \Seen' ) ; + + is( q{\Deleted \Seen}, flags_for_host2( $mysync, '\Deleted \Recent \Seen' ), 'flags_for_host2: \Deleted \Recent \Seen => \Deleted \Seen' ) ; + + $mysync->{ flagscase } = 0 ; + is( q{\DELETED \Seen}, flags_for_host2( $mysync, '\DELETED \Seen' ), 'flags_for_host2: flagscase = 0 \DELETED \Seen => \DELETED \Seen' ) ; + + $mysync->{ flagscase } = 1 ; + is( q{\Deleted \Seen}, flags_for_host2( $mysync, '\DELETED \Seen' ), 'flags_for_host2: flagscase = 1 \DELETED \Seen => \Deleted \Seen' ) ; + + $mysync->{ filterflags } = 0 ; + is( q{\Seen \Blabla}, flags_for_host2( $mysync, '\Seen \Blabla', '\Seen \Junk' ), 'flags_for_host2: filterflags = 0 \Seen \Blabla among \Seen \Junk => \Seen \Blabla' ) ; + + $mysync->{ filterflags } = 1 ; + is( q{\Seen}, flags_for_host2( $mysync, '\Seen \Blabla', '\Seen \Junk' ), 'flags_for_host2: filterflags = 1 \Seen \Blabla among \Seen \Junk => \Seen' ) ; + + $mysync->{ filterflags } = 1 ; + is( q{\Seen \Blabla}, flags_for_host2( $mysync, '\Seen \Blabla', '' ), 'flags_for_host2: filterflags = 1 \Seen \Blabla among "" => \Seen \Blabla' ) ; + + + note( 'Leaving tests_flags_for_host2()' ) ; + return ; +} + + + + +sub flags_for_host2 +{ + my $mysync = shift ; + my $h1_flags = shift ; + my $permanentflags2 = shift ; + + if ( ! all_defined( $mysync, $h1_flags ) ) { return ; } ; + + # RFC 2060: This flag can not be altered by any client + $h1_flags =~ s@\\Recent\s?@@xgi ; + + my $h1_flags_re ; + if ( $mysync->{ regexflag } and defined( $h1_flags_re = regexflags( $mysync, $h1_flags ) ) ) { + $h1_flags = $h1_flags_re ; + } + + if ( $mysync->{ flagscase } ) + { + $h1_flags = flagscase( $h1_flags ) ; + } + + if ( $permanentflags2 and $mysync->{ filterflags } ) + { + $h1_flags = flags_filter( $h1_flags, $permanentflags2 ) ; + } + + return( $h1_flags ) ; +} + + + +sub ucsecond +{ + my $string = shift ; + my $output ; + + return( $string ) if ( 1 >= length $string ) ; + + $output = ( substr( $string, 0, 1) ) . ( uc substr $string, 1, 1 ) . ( substr $string, 2 ) ; + #myprint( "UUU $string -> $output\n" ) ; + return( $output ) ; +} + + +sub tests_ucsecond +{ + note( 'Entering tests_ucsecond()' ) ; + + ok( 'aBcde' eq ucsecond( 'abcde' ), 'ucsecond: abcde -> aBcde' ) ; + ok( 'ABCDE' eq ucsecond( 'ABCDE' ), 'ucsecond: ABCDE -> ABCDE' ) ; + ok( 'ABCDE' eq ucsecond( 'AbCDE' ), 'ucsecond: AbCDE -> ABCDE' ) ; + ok( 'ABCde' eq ucsecond( 'AbCde' ), 'ucsecond: AbCde -> ABCde' ) ; + ok( 'A' eq ucsecond( 'A' ), 'ucsecond: A -> A' ) ; + ok( 'AB' eq ucsecond( 'Ab' ), 'ucsecond: Ab -> AB' ) ; + ok( '\B' eq ucsecond( '\b' ), 'ucsecond: \b -> \B' ) ; + ok( '\Bcde' eq ucsecond( '\bcde' ), 'ucsecond: \bcde -> \Bcde' ) ; + + note( 'Leaving tests_ucsecond()' ) ; + return ; +} + + +sub select_msgs +{ + my ( $imap, $msgs_all_hash_ref, $search_cmd, $abletosearch, $folder ) = @_ ; + my ( @msgs ) ; + + if ( $abletosearch ) { + @msgs = select_msgs_by_search( $imap, $msgs_all_hash_ref, $search_cmd, $folder ) ; + }else{ + @msgs = select_msgs_by_fetch( $imap, $msgs_all_hash_ref, $search_cmd, $folder ) ; + } + return( @msgs ) ; + +} + +sub select_msgs_by_search +{ + my ( $imap, $msgs_all_hash_ref, $search_cmd, $folder ) = @_ ; + my ( @msgs, @msgs_all ) ; + + # Need to have the whole list in msgs_all_hash_ref + # without calling messages() several times. + # Need all messages list to avoid deleting useful cache part + # in case of --search or --minage or --maxage + + if ( ( defined $msgs_all_hash_ref and $usecache ) + or ( not defined $maxage and not defined $minage and not defined $search_cmd ) + ) { + + $debugdev and myprint( "Calling messages()\n" ) ; + @msgs_all = $imap->messages( ) ; + + return if ( $#msgs_all == 0 && !defined $msgs_all[0] ) ; + + if ( defined $msgs_all_hash_ref ) { + @{ $msgs_all_hash_ref }{ @msgs_all } = () ; + } + # return all messages + if ( not defined $maxage and not defined $minage and not defined $search_cmd ) { + return( @msgs_all ) ; + } + } + + if ( defined $search_cmd ) { + @msgs = $imap->search( $search_cmd ) ; + return( @msgs ) ; + } + + # we are here only if $maxage or $minage is defined + @msgs = select_msgs_by_age( $imap ) ; + return( @msgs ); +} + + +sub select_msgs_by_fetch +{ + my ( $imap, $msgs_all_hash_ref, $search_cmd, $folder ) = @_ ; + my ( @msgs, @msgs_all, %fetch ) ; + + # Need to have the whole list in msgs_all_hash_ref + # without calling messages() several times. + # Need all messages list to avoid deleting useful cache part + # in case of --search or --minage or --maxage + + + $debugdev and myprint( "Calling fetch_hash()\n" ) ; + my $fetch_hash_uids = $fetch_hash_set || "1:*" ; + %fetch = %{$imap->fetch_hash( $fetch_hash_uids, 'INTERNALDATE' ) } ; + + @msgs_all = sort { $a <=> $b } keys %fetch ; + $debugdev and myprint( "Done fetch_hash()\n" ) ; + + return if ( $#msgs_all == 0 && !defined $msgs_all[0] ) ; + + if ( defined $msgs_all_hash_ref ) { + @{ $msgs_all_hash_ref }{ @msgs_all } = () ; + } + # return all messages + if ( not defined $maxage and not defined $minage and not defined $search_cmd ) { + return( @msgs_all ) ; + } + + if ( defined $search_cmd ) { + myprint( "Warning: strange to see --search with --noabletosearch, an error can happen\n" ) ; + @msgs = $imap->search( $search_cmd ) ; + return( @msgs ) ; + } + + # we are here only if $maxage or $minage is defined + my( @max, @min, $maxage_epoch, $minage_epoch ) ; + if ( defined $maxage ) { $maxage_epoch = $timestart_int - $NB_SECONDS_IN_A_DAY * $maxage ; } + if ( defined $minage ) { $minage_epoch = $timestart_int - $NB_SECONDS_IN_A_DAY * $minage ; } + foreach my $msg ( @msgs_all ) { + my $idate = $fetch{ $msg }->{'INTERNALDATE'} ; + #myprint( "$idate\n" ) ; + if ( defined $maxage and ( epoch( $idate ) >= $maxage_epoch ) ) { + push @max, $msg ; + } + if ( defined $minage and ( epoch( $idate ) <= $minage_epoch ) ) { + push @min, $msg ; + } + } + @msgs = msgs_from_maxmin( \@max, \@min ) ; + return( @msgs ) ; +} + +sub select_msgs_by_age +{ + my( $imap ) = @_ ; + + my( @max, @min, @msgs, @inter, @union ) ; + + if ( defined $maxage ) { + @max = $imap->sentsince( $timestart_int - $NB_SECONDS_IN_A_DAY * $maxage ) ; + } + if ( defined $minage ) { + @min = $imap->sentbefore( $timestart_int - $NB_SECONDS_IN_A_DAY * $minage ) ; + } + + @msgs = msgs_from_maxmin( \@max, \@min ) ; + return( @msgs ) ; +} + +sub msgs_from_maxmin +{ + my( $max_ref, $min_ref ) = @_ ; + my( @max, @min, @msgs, @inter, @union ) ; + + @max = @{ $max_ref } ; + @min = @{ $min_ref } ; + + SWITCH: { + if ( not ( defined $minage or defined $maxage ) ) + { + return ; + } + unless( defined $minage ) { @msgs = @max ; last SWITCH } ; + unless( defined $maxage ) { @msgs = @min ; last SWITCH } ; + my ( %union, %inter ) ; + foreach my $m ( @min, @max ) { $union{ $m }++ && $inter{ $m }++ } + @inter = sort { $a <=> $b } keys %inter ; + @union = sort { $a <=> $b } keys %union ; + # normal case + if ( $minage <= $maxage ) { @msgs = @inter ; last SWITCH } ; + # just exclude messages between + if ( $minage > $maxage ) { @msgs = @union ; last SWITCH } ; + + } + return( @msgs ) ; +} + +sub tests_msgs_from_maxmin +{ + note( 'Entering tests_msgs_from_maxmin()' ) ; + + + my @msgs ; + + # no maxage nor minage + @msgs = msgs_from_maxmin( [ '1', '2' ], [ '2', '3' ] ) ; + is_deeply( [ ], \@msgs , 'msgs_from_maxmin: no maxage nor minage => empty result' ) ; + + # maxage alone + $maxage = $NUMBER_200 ; + @msgs = msgs_from_maxmin( [ '1', '2' ], [ '2', '3' ] ) ; + is_deeply( [ '1', '2' ], \@msgs , 'msgs_from_maxmin: maxage++' ) ; + + # maxage > minage -> intersection + $minage = $NUMBER_100 ; + @msgs = msgs_from_maxmin( [ '1', '2' ], [ '2', '3' ] ) ; + is_deeply( [ '2' ], \@msgs , 'msgs_from_maxmin: -maxage++minage-' ) ; + + # maxage < minage -> union + $minage = $NUMBER_300 ; + @msgs = msgs_from_maxmin( [ '1', '2' ], [ '2', '3' ] ) ; + is_deeply( [ '1', '2', '3' ], \@msgs, 'msgs_from_maxmin: ++maxage-minage++' ) ; + + + # minage alone + $maxage = undef ; + @msgs = msgs_from_maxmin( [ '1', '2' ], [ '2', '3' ] ) ; + is_deeply( [ '2', '3' ], \@msgs, 'msgs_from_maxmin: ++minage-' ) ; + + + note( 'Leaving tests_msgs_from_maxmin()' ) ; + return ; +} + +sub tests_info_date_from_uid +{ + note( 'Entering tests_info_date_from_uid()' ) ; + note( 'Leaving tests_info_date_from_uid()' ) ; + + return ; +} + +sub info_date_from_uid +{ + + #my $first_uid = $msgs_all[ 0 ] ; + #my $first_idate = $fetch{ $first_uid }->{'INTERNALDATE'} ; + #my $first_epoch = epoch( $first_idate ) ; + #my $first_days = ( $timestart_int - $first_epoch ) / $NB_SECONDS_IN_A_DAY ; + #myprint( "\nOldest msg has UID $first_uid INTERNALDATE $first_idate EPOCH $first_epoch DAYS AGO $first_days\n" ) ; +} + + +sub lastuid +{ + my $imap = shift ; + my $folder = shift ; + my $lastuid_guess = shift ; + my $lastuid ; + + # rfc3501: The only reliable way to identify recent messages is to + # look at message flags to see which have the \Recent flag + # set, or to do a SEARCH RECENT. + # SEARCH RECENT doesn't work this way on courrier. + + my @recent_messages ; + # SEARCH RECENT for each transfer can be expensive with a big folder + # Call commented for now + #@recent_messages = $imap->recent( ) ; + #myprint( "Recent: @recent_messages\n" ) ; + + my $max_recent ; + $max_recent = max( @recent_messages ) ; + + if ( defined $max_recent and ($lastuid_guess <= $max_recent ) ) { + $lastuid = $max_recent ; + }else{ + $lastuid = $lastuid_guess + } + return( $lastuid ) ; +} + +sub size_filtered +{ + my( $h1_size, $h1_msg, $h1_fold, $h2_fold ) = @_ ; + + $h1_size = 0 if ( ! $h1_size ) ; # null if empty or undef + if ( defined $sync->{ maxsize } and $h1_size > $sync->{ maxsize } ) { + myprint( "msg $h1_fold/$h1_msg skipped ($h1_size exceeds maxsize limit $sync->{ maxsize } bytes)\n" ) ; + $sync->{ total_bytes_skipped } += $h1_size; + $sync->{ nb_msg_skipped } += 1; + return( 1 ) ; + } + if ( defined $minsize and $h1_size <= $minsize ) { + myprint( "msg $h1_fold/$h1_msg skipped ($h1_size smaller than minsize $minsize bytes)\n" ) ; + $sync->{ total_bytes_skipped } += $h1_size; + $sync->{ nb_msg_skipped } += 1; + return( 1 ) ; + } + return( 0 ) ; +} + +sub message_exists +{ + my( $imap, $msg ) = @_ ; + return( 1 ) if not $imap->Uid( ) ; + + my $search_uid ; + ( $search_uid ) = $imap->search( "UID $msg" ) ; + #myprint( "$search ? $msg\n" ) ; + return( 1 ) if ( $search_uid eq $msg ) ; + return( 0 ) ; +} + + +# Globals +# $sync->{ total_bytes_skipped } +# $sync->{ nb_msg_skipped } +# $mysync->{ h1_nb_msg_processed } +sub stats_update_skip_message +{ + my $mysync = shift ; # to be used + my $h1_size = shift ; + + $mysync->{ total_bytes_skipped } += $h1_size ; + $mysync->{ nb_msg_skipped } += 1 ; + $mysync->{ h1_nb_msg_processed } +=1 ; + return ; +} + +sub copy_message +{ + # copy + + my ( $mysync, $h1_msg, $h1_fold, $h2_fold, $h1_fir_ref, $permanentflags2, $cache_dir ) = @_ ; + ( $mysync->{ debug } or $mysync->{dry} ) + and myprint( "msg $h1_fold/$h1_msg copying to $h2_fold $mysync->{dry_message} " . eta( $mysync ) . "\n" ) ; + + if ( $mysync->{dry1} ) + { + $mysync->{ h1_nb_msg_processed } +=1 ; + $nb_msg_skipped_dry_mode += 1 ; + return ; + } + + my $h1_size = $h1_fir_ref->{$h1_msg}->{'RFC822.SIZE'} || 0 ; + my $h1_flags = $h1_fir_ref->{$h1_msg}->{'FLAGS'} || q{} ; + my $h1_idate = $h1_fir_ref->{$h1_msg}->{'INTERNALDATE'} || q{} ; + + + if ( size_filtered( $h1_size, $h1_msg, $h1_fold, $h2_fold ) ) { + $mysync->{ h1_nb_msg_processed } +=1 ; + return ; + } + + debugsleep( $mysync ) ; + myprint( "- msg $h1_fold/$h1_msg S[$h1_size] F[$h1_flags] I[$h1_idate] has RFC822.SIZE null!\n" ) if ( ! $h1_size ) ; + + if ( $checkmessageexists and not message_exists( $mysync->{imap1}, $h1_msg ) ) { + stats_update_skip_message( $mysync, $h1_size ) ; + return ; + } + myprint( debugmemory( $mysync, " at C1" ) ) ; + + my ( $string, $string_len ) ; + ( $string_len ) = message_for_host2( $mysync, $h1_msg, $h1_fold, $h1_size, $h1_flags, $h1_idate, $h1_fir_ref, \$string ) ; + + myprint( debugmemory( $mysync, " at C2" ) ) ; + + # not defined or empty $string + if ( ( not $string ) or ( not $string_len ) ) { + myprint( "- msg $h1_fold/$h1_msg skipped.\n" ) ; + stats_update_skip_message( $mysync, $h1_size ) ; + return ; + } + + # Lines too long (or not enough) => do no copy or fix + if ( ( defined $maxlinelength ) or ( defined $minmaxlinelength ) ) { + $string = linelengthstuff( $string, $h1_fold, $h1_msg, $string_len, $h1_size, $h1_flags, $h1_idate ) ; + if ( not defined $string ) { + stats_update_skip_message( $mysync, $h1_size ) ; + return ; + } + } + + my $h1_date = date_for_host2( $h1_msg, $h1_idate ) ; + + ( $mysync->{ debug } or $debugflags ) and + myprint( "Host1: flags init msg $h1_fold/$h1_msg date [$h1_date] flags [$h1_flags] size [$h1_size]\n" ) ; + + $h1_flags = flags_for_host2( $mysync, $h1_flags, $permanentflags2 ) ; + + ( $mysync->{ debug } or $debugflags ) and + myprint( "Host1: flags filt msg $h1_fold/$h1_msg date [$h1_date] flags [$h1_flags] size [$h1_size]\n" ) ; + + $h1_date = undef if ( $h1_date eq q{} ) ; + + my $new_id = append_message_on_host2( $mysync, \$string, $h1_fold, $h1_msg, $string_len, $h2_fold, $h1_size, $h1_flags, $h1_date, $cache_dir ) ; + + + + if ( $new_id and $syncflagsaftercopy ) { + sync_flags_after_copy( $mysync, $h1_fold, $h1_msg, $h1_flags, $h2_fold, $new_id, $permanentflags2 ) ; + } + + myprint( debugmemory( $mysync, " at C3" ) ) ; + + return $new_id ; +} + + + +sub linelengthstuff +{ + my( $string, $h1_fold, $h1_msg, $string_len, $h1_size, $h1_flags, $h1_idate ) = @_ ; + my $maxlinelength_string = max_line_length( $string ) ; + $debugmaxlinelength and myprint( "msg $h1_fold/$h1_msg maxlinelength: $maxlinelength_string\n" ) ; + + if ( ( defined $minmaxlinelength ) and ( $maxlinelength_string <= $minmaxlinelength ) ) { + my $subject = subject( $string ) ; + $debugdev and myprint( "- msg $h1_fold/$h1_msg skipped S[$h1_size] F[$h1_flags] I[$h1_idate] " + . "(Subject:[$subject]) (max line length under minmaxlinelength $minmaxlinelength bytes)\n" ) ; + return ; + } + + if ( ( defined $maxlinelength ) and ( $maxlinelength_string > $maxlinelength ) ) { + my $subject = subject( $string ) ; + if ( $maxlinelengthcmd ) { + $string = pipemess( $string, $maxlinelengthcmd ) ; + # string undef means something was bad. + if ( not ( defined $string ) ) { + myprint( "- msg $h1_fold/$h1_msg {$string_len} S[$h1_size] F[$h1_flags] I[$h1_idate] " + . "(Subject:[$subject]) could not be successfully transformed by --maxlinelengthcmd option\n" ) ; + return ; + }else{ + return $string ; + } + } + myprint( "- msg $h1_fold/$h1_msg skipped S[$h1_size] F[$h1_flags] I[$h1_idate] " + . "(Subject:[$subject]) (line length exceeds maxlinelength $maxlinelength bytes)\n" ) ; + return ; + } + return $string ; +} + + +sub message_for_host2 +{ + +# global variable list: +# @skipmess +# @regexmess +# @pipemess +# $debugcontent +# $debug +# +# API current +# +# at failure: +# * return nothing ( will then be undef or () ) +# * $string_ref content is undef or empty +# at success: +# * return string length ($string_ref content length) +# * $string_ref content filled with message + +# API future +# +# + my ( $mysync, $h1_msg, $h1_fold, $h1_size, $h1_flags, $h1_idate, $h1_fir_ref, $string_ref ) = @_ ; + + # abort when missing a parameter + if ( ( ! $mysync ) or ( ! $h1_msg ) or ( ! $h1_fold ) or ( ! defined $h1_size ) + or ( ! defined $h1_flags) or ( ! defined $h1_idate ) + or ( ! $h1_fir_ref) or ( ! $string_ref ) ) + { + return ; + } + + myprint( debugmemory( $mysync, " at M1" ) ) ; + + + my $string_ok = $mysync->{imap1}->message_to_file( $string_ref, $h1_msg ) ; + + myprint( debugmemory( $mysync, " at M2" ) ) ; + + my $string_len = length_ref( $string_ref ) ; + + + unless ( defined $string_ok and $string_len ) { + # undef or 0 length + my $error = join q{}, + "- msg $h1_fold/$h1_msg {$string_len} S[$h1_size] F[$h1_flags] I[$h1_idate] could not be fetched: ", + $mysync->{imap1}->LastError || q{}, "\n" ; + errors_incr( $mysync, $error ) ; + $mysync->{ h1_nb_msg_processed } +=1 ; + return ; + } + + if ( @skipmess ) { + my $match = skipmess( ${ $string_ref } ) ; + # string undef means the eval regex was bad. + if ( not ( defined $match ) ) { + myprint( + "- msg $h1_fold/$h1_msg {$string_len} S[$h1_size] F[$h1_flags] I[$h1_idate]" + . " could not be skipped by --skipmess option, bad regex\n" ) ; + return ; + } + if ( $match ) { + my $subject = subject( ${ $string_ref } ) ; + myprint( "- msg $h1_fold/$h1_msg {$string_len} S[$h1_size] F[$h1_flags] I[$h1_idate]" + . " (Subject:[$subject]) skipped by --skipmess\n" ) ; + return ; + } + } + + if ( @regexmess ) { + ${ $string_ref } = regexmess( ${ $string_ref } ) ; + # string undef means the eval regex was bad. + if ( not ( defined ${ $string_ref } ) ) { + myprint( + "- msg $h1_fold/$h1_msg {$string_len} S[$h1_size] F[$h1_flags] I[$h1_idate]" + . " could not be transformed by --regexmess\n" ) ; + return ; + } + } + + if ( @pipemess ) { + ${ $string_ref } = pipemess( ${ $string_ref }, @pipemess ) ; + # string undef means something was bad. + if ( not ( defined ${ $string_ref } ) ) { + myprint( + "- msg $h1_fold/$h1_msg {$string_len} S[$h1_size] F[$h1_flags] I[$h1_idate]" + . " could not be successfully transformed by --pipemess option\n" ) ; + return ; + } + } + + if ( $mysync->{addheader} and defined $h1_fir_ref->{$h1_msg}->{'NO_HEADER'} ) { + my $header = add_header( $h1_msg ) ; + $mysync->{ debug } and myprint( "msg $h1_fold/$h1_msg adding custom header [$header]\n" ) ; + ${ $string_ref } = $header . "\r\n" . ${ $string_ref } ; + } + + if ( ( defined $mysync->{ truncmess } ) and is_integer( $mysync->{ truncmess } ) ) + { + ${ $string_ref } = truncmess( ${ $string_ref }, $mysync->{ truncmess } ) ; + } + + $string_len = length_ref( $string_ref ) ; + + $mysync->{ debugcontent } and myprint( debugcontent( $mysync, $string_ref ) ) ; + + myprint( debugmemory( $mysync, " at M3" ) ) ; + + return $string_len ; +} + +sub tests_debugcontent +{ + note( 'Entering tests_debugcontent()' ) ; + + is( undef, debugcontent( ), 'debugcontent: no args => undef' ) ; + my $mysync = { } ; + is( undef, debugcontent( $mysync ), 'debugcontent: undef => undef' ) ; + is( undef, debugcontent( $mysync, 'mm' ), 'debugcontent: undef, mm => undef' ) ; + #my $string_ref = \'zztop' ; + my $string = '================================================================================ +F message content begin next line (2 characters long) +mm +F message content ended on previous line +================================================================================ +' ; + is( $string, debugcontent( $mysync, \'mm' ), 'debugcontent: undef, mm => mm' ) ; + + note( 'Leaving tests_debugcontent()' ) ; + return ; +} + +sub debugcontent +{ + my $mysync = shift @ARG ; + if ( ! defined $mysync ) { return ; } + + my $string_ref = shift @ARG ; + if ( ! defined $string_ref ) { return ; } + if ( 'SCALAR' ne ref( $string_ref ) ) { return ; } + + my $string_len = length_ref( $string_ref ) ; + + my $string = join( '', + q{=} x $STD_CHAR_PER_LINE, "\n", + "F message content begin next line ($string_len characters long)\n", + ${ $string_ref }, + "\nF message content ended on previous line\n", q{=} x $STD_CHAR_PER_LINE, "\n", + ) ; + + return $string ; +} + + + + + +sub tests_truncmess +{ + note( 'Entering tests_truncmess()' ) ; + + is( undef, truncmess( ), 'truncmess: no args => undef' ) ; + is( 'abc', truncmess( 'abc' ), 'truncmess: abc => abc' ) ; + is( 'ab', truncmess( 'abc', 2 ), 'truncmess: abc 2 => ab' ) ; + is( 'abc', truncmess( 'abc', 3 ), 'truncmess: abc 3 => abc' ) ; + is( 'abc', truncmess( 'abc', 4 ), 'truncmess: abc 4 => abc' ) ; + is( '12345', truncmess( "123456789\n", 5 ), 'truncmess: "123456789\n", 5 => 12345' ) ; + is( "123456789\n" x 5000, truncmess( "123456789\n" x 100000, 50000 ), 'truncmess: "123456789\n" x 100000, 50000 => "123456789\n" x 5000' ) ; + note( 'Leaving tests_truncmess()' ) ; + return ; +} + +sub truncmess +{ + my $string = shift ; + my $length = shift ; + + if ( not defined $string ) { return ; } + if ( not defined $length ) { return $string ; } + + $string = substr $string, 0, $length ; + return $string ; +} + +sub tests_message_for_host2 +{ + note( 'Entering tests_message_for_host2()' ) ; + + + my ( $mysync, $h1_msg, $h1_fold, $h1_size, $h1_flags, $h1_idate, $h1_fir_ref, $string_ref ) ; + + is( undef, message_for_host2( ), q{message_for_host2: no args} ) ; + is( undef, message_for_host2( $mysync, $h1_msg, $h1_fold, $h1_size, $h1_flags, $h1_idate, $h1_fir_ref, $string_ref ), q{message_for_host2: undef args} ) ; + + require_ok( "Test::MockObject" ) ; + my $imapT = Test::MockObject->new( ) ; + $mysync->{imap1} = $imapT ; + my $string ; + + $h1_msg = 1 ; + $h1_fold = 'FoldFoo'; + $h1_size = 9 ; + $h1_flags = q{} ; + $h1_idate = '10-Jul-2015 09:00:00 +0200' ; + $h1_fir_ref = {} ; + $string_ref = \$string ; + $imapT->mock( 'message_to_file', + sub { + my ( $imap, $mystring_ref, $msg ) = @_ ; + ${$mystring_ref} = 'blablabla' ; + return length ${$mystring_ref} ; + } + ) ; + is( 9, message_for_host2( $mysync, $h1_msg, $h1_fold, $h1_size, $h1_flags, $h1_idate, $h1_fir_ref, $string_ref ), + q{message_for_host2: msg 1 == "blablabla", length} ) ; + is( 'blablabla', $string, q{message_for_host2: msg 1 == "blablabla", value} ) ; + + # so far so good + # now the --pipemess stuff + + SKIP: { + Readonly my $NB_WIN_tests_message_for_host2 => 0 ; + skip( 'Not on MSWin32', $NB_WIN_tests_message_for_host2 ) if ('MSWin32' ne $OSNAME) ; + # Windows + # "type" command does not accept redirection of STDIN with < + # "sort" does + + } ; + + SKIP: { + Readonly my $NB_UNX_tests_message_for_host2 => 6 ; + skip( 'Not on Unix', $NB_UNX_tests_message_for_host2 ) if ('MSWin32' eq $OSNAME) ; + # Unix + + # no change by cat + @pipemess = ( 'cat' ) ; + is( 9, message_for_host2( $mysync, $h1_msg, $h1_fold, $h1_size, $h1_flags, $h1_idate, $h1_fir_ref, $string_ref ), + q{message_for_host2: --pipemess 'cat', length} ) ; + is( 'blablabla', $string, q{message_for_host2: --pipemess 'cat', value} ) ; + + + # failure by false + @pipemess = ( 'false' ) ; + is( undef, message_for_host2( $mysync, $h1_msg, $h1_fold, $h1_size, $h1_flags, $h1_idate, $h1_fir_ref, $string_ref ), + q{message_for_host2: --pipemess 'false', length} ) ; + is( undef, $string, q{message_for_host2: --pipemess 'false', value} ) ; + + # failure by true since no output + @pipemess = ( 'true' ) ; + is( undef, message_for_host2( $mysync, $h1_msg, $h1_fold, $h1_size, $h1_flags, $h1_idate, $h1_fir_ref, $string_ref ), + q{message_for_host2: --pipemess 'true', length} ) ; + is( undef, $string, q{message_for_host2: --pipemess 'true', value} ) ; + } + + note( 'Leaving tests_message_for_host2()' ) ; + return ; +} + +sub tests_labels_remove_subfolder1 +{ + note( 'Entering tests_labels_remove_subfolder1()' ) ; + is( undef, labels_remove_subfolder1( ), 'labels_remove_subfolder1: no parameters => undef' ) ; + is( 'Blabla', labels_remove_subfolder1( 'Blabla' ), 'labels_remove_subfolder1: one parameter Blabla => Blabla' ) ; + is( 'Blan blue', labels_remove_subfolder1( 'Blan blue' ), 'labels_remove_subfolder1: one parameter Blan blue => Blan blue' ) ; + is( '\Bla "Blan blan" Blabla', labels_remove_subfolder1( '\Bla "Blan blan" Blabla' ), + 'labels_remove_subfolder1: one parameter \Bla "Blan blan" Blabla => \Bla "Blan blan" Blabla' ) ; + + is( 'Bla', labels_remove_subfolder1( 'Subf/Bla', 'Subf' ), 'labels_remove_subfolder1: Subf/Bla Subf => "Bla"' ) ; + + + is( '"\\\\Bla"', labels_remove_subfolder1( '"\\\\Bla"', 'Subf' ), 'labels_remove_subfolder1: "\\\\Bla" Subf => "\\\\Bla"' ) ; + + is( 'Bla Kii', labels_remove_subfolder1( 'Subf/Bla Subf/Kii', 'Subf' ), + 'labels_remove_subfolder1: Subf/Bla Subf/Kii, Subf => "Bla" "Kii"' ) ; + + is( '"\\\\Bla" Kii', labels_remove_subfolder1( '"\\\\Bla" Subf/Kii', 'Subf' ), + 'labels_remove_subfolder1: "\\\\Bla" Subf/Kii Subf => "\\\\Bla" Kii' ) ; + + is( '"Blan blan"', labels_remove_subfolder1( '"Subf/Blan blan"', 'Subf' ), + 'labels_remove_subfolder1: "Subf/Blan blan" Subf => "Blan blan"' ) ; + + is( '"\\\\Loo" "Blan blan" Kii', labels_remove_subfolder1( '"\\\\Loo" "Subf/Blan blan" Subf/Kii', 'Subf' ), + 'labels_remove_subfolder1: "\\\\Loo" "Subf/Blan blan" Subf/Kii + Subf => "\\\\Loo" "Blan blan" Kii' ) ; + + is( '"\\\\Inbox"', labels_remove_subfolder1( 'Subf/INBOX', 'Subf' ), + 'labels_remove_subfolder1: Subf/INBOX + Subf => "\\\\Inbox"' ) ; + + is( '"\\\\Loo" "Blan blan" Kii "\\\\Inbox"', labels_remove_subfolder1( '"\\\\Loo" "Subf/Blan blan" Subf/Kii Subf/INBOX', 'Subf' ), + 'labels_remove_subfolder1: "\\\\Loo" "Subf/Blan blan" Subf/Kii Subf/INBOX + Subf => "\\\\Loo" "Blan blan" Kii "\\\\Inbox"' ) ; + + + note( 'Leaving tests_labels_remove_subfolder1()' ) ; + return ; +} + + + +sub labels_remove_subfolder1 +{ + my $labels = shift ; + my $subfolder1 = shift ; + + if ( not defined $labels ) { return ; } + if ( not defined $subfolder1 ) { return $labels ; } + + my @labels = quotewords('\s+', 1, $labels ) ; + #myprint( "@labels\n" ) ; + my @labels_subfolder2 ; + + foreach my $label ( @labels ) + { + if ( $label =~ m{zzzzzzzzzz} ) + { + # \Seen \Deleted ... stay the same + push @labels_subfolder2, $label ; + } + else + { + # Remove surrounding quotes if any, to add them again in case of space + $label = join( q{}, quotewords('\s+', 0, $label ) ) ; + $label =~ s{$subfolder1/?}{} ; + if ( 'INBOX' eq $label ) + { + push @labels_subfolder2, q{"\\\\Inbox"} ; + } + elsif ( $label =~ m{\\} ) + { + push @labels_subfolder2, qq{"\\$label"} ; + } + elsif ( $label =~ m{ } ) + { + push @labels_subfolder2, qq{"$label"} ; + } + else + { + push @labels_subfolder2, $label ; + } + } + } + + my $labels_subfolder2 = join( ' ', sort uniq( @labels_subfolder2 ) ) ; + + return $labels_subfolder2 ; +} + +sub tests_labels_remove_special +{ + note( 'Entering tests_labels_remove_special()' ) ; + + is( undef, labels_remove_special( ), 'labels_remove_special: no parameters => undef' ) ; + is( q{}, labels_remove_special( q{} ), 'labels_remove_special: empty string => empty string' ) ; + is( q{}, labels_remove_special( '"\\\\Inbox"' ), 'labels_remove_special:"\\\\Inbox" => empty string' ) ; + is( q{}, labels_remove_special( '"\\\\Inbox" "\\\\Starred"' ), 'labels_remove_special:"\\\\Inbox" "\\\\Starred" => empty string' ) ; + is( 'Bar Foo', labels_remove_special( 'Foo Bar' ), 'labels_remove_special:Foo Bar => Bar Foo' ) ; + is( 'Bar Foo', labels_remove_special( 'Foo Bar "\\\\Inbox"' ), 'labels_remove_special:Foo Bar "\\\\Inbox" => Bar Foo' ) ; + note( 'Leaving tests_labels_remove_special()' ) ; + return ; +} + + + + +sub labels_remove_special +{ + my $labels = shift ; + + if ( not defined $labels ) { return ; } + + my @labels = quotewords('\s+', 1, $labels ) ; + myprint( "labels before remove_non_folded: @labels\n" ) ; + my @labels_remove_special ; + + foreach my $label ( @labels ) + { + if ( $label =~ m{^\"\\\\} ) + { + # not kept + } + else + { + push @labels_remove_special, $label ; + } + } + + my $labels_remove_special = join( ' ', sort @labels_remove_special ) ; + + return $labels_remove_special ; +} + + +sub tests_labels_add_subfolder2 +{ + note( 'Entering tests_labels_add_subfolder2()' ) ; + is( undef, labels_add_subfolder2( ), 'labels_add_subfolder2: no parameters => undef' ) ; + is( 'Blabla', labels_add_subfolder2( 'Blabla' ), 'labels_add_subfolder2: one parameter Blabla => Blabla' ) ; + is( 'Blan blue', labels_add_subfolder2( 'Blan blue' ), 'labels_add_subfolder2: one parameter Blan blue => Blan blue' ) ; + is( '\Bla "Blan blan" Blabla', labels_add_subfolder2( '\Bla "Blan blan" Blabla' ), + 'labels_add_subfolder2: one parameter \Bla "Blan blan" Blabla => \Bla "Blan blan" Blabla' ) ; + + is( 'Subf/Bla', labels_add_subfolder2( 'Bla', 'Subf' ), 'labels_add_subfolder2: Bla Subf => "Subf/Bla"' ) ; + + + is( 'Subf/\Bla', labels_add_subfolder2( '\\\\Bla', 'Subf' ), 'labels_add_subfolder2: \Bla Subf => \Bla' ) ; + + is( 'Subf/Bla Subf/Kii', labels_add_subfolder2( 'Bla Kii', 'Subf' ), + 'labels_add_subfolder2: Bla Kii Subf => "Subf/Bla" "Subf/Kii"' ) ; + + is( 'Subf/Kii Subf/\Bla', labels_add_subfolder2( '\\\\Bla Kii', 'Subf' ), + 'labels_add_subfolder2: \Bla Kii Subf => \Bla Subf/Kii' ) ; + + is( '"Subf/Blan blan"', labels_add_subfolder2( '"Blan blan"', 'Subf' ), + 'labels_add_subfolder2: "Blan blan" Subf => "Subf/Blan blan"' ) ; + + is( '"Subf/Blan blan" Subf/Kii Subf/\Loo', labels_add_subfolder2( '\\\\Loo "Blan blan" Kii', 'Subf' ), + 'labels_add_subfolder2: \Loo "Blan blan" Kii + Subf => "Subf/Blan blan" Subf/Kii Subf/\Loo' ) ; + + # "\\Inbox" is special, add to subfolder INBOX also because Gmail will but ... + is( '"Subf/\\\\Inbox" Subf/INBOX', labels_add_subfolder2( '"\\\\Inbox"', 'Subf' ), + 'labels_add_subfolder2: "\\\\Inbox" Subf => "Subf/\\\\Inbox" Subf/INBOX' ) ; + + # but not with INBOX folder + is( '"Subf/\\\\Inbox"', labels_add_subfolder2( '"\\\\Inbox"', 'Subf', 'INBOX' ), + 'labels_add_subfolder2: "\\\\Inbox" Subf INBOX => "Subf/\\\\Inbox"' ) ; + + # two times => one time + is( '"Subf/\\\\Inbox" Subf/INBOX', labels_add_subfolder2( '"\\\\Inbox" "\\\\Inbox"', 'Subf' ), + 'labels_add_subfolder2: "\\\\Inbox" "\\\\Inbox" Subf => "Subf/\\\\Inbox"' ) ; + + is( '"Subf/\\\\Starred"', labels_add_subfolder2( '"\\\\Starred"', 'Subf' ), + 'labels_add_subfolder2: "\\\\Starred" Subf => "Subf/\\\\Starred"' ) ; + + note( 'Leaving tests_labels_add_subfolder2()' ) ; + return ; +} + +sub labels_add_subfolder2 +{ + my $labels = shift ; + my $subfolder2 = shift ; + my $h1_folder = shift || q{} ; + + if ( not defined $labels ) { return ; } + if ( not defined $subfolder2 ) { return $labels ; } + + # Isn't it messy? + if ( 'INBOX' eq $h1_folder ) + { + $labels .= ' "\\\\Inbox"' ; + } + + my @labels = uniq( quotewords('\s+', 1, $labels ) ) ; + myprint( "labels before subfolder2: @labels\n" ) ; + my @labels_subfolder2 ; + + + foreach my $label ( @labels ) + { + # Isn't it more messy? + if ( ( q{"\\\\Inbox"} eq $label ) and ( 'INBOX' ne $h1_folder ) ) + { + if ( $subfolder2 =~ m{ } ) + { + push @labels_subfolder2, qq{"$subfolder2/INBOX"} ; + } + else + { + push @labels_subfolder2, "$subfolder2/INBOX" ; + } + } + if ( $label =~ m{^\"\\\\} ) + { + # \Seen \Deleted ... stay the same + #push @labels_subfolder2, $label ; + # Remove surrounding quotes if any, to add them again + $label = join( q{}, quotewords('\s+', 0, $label ) ) ; + push @labels_subfolder2, qq{"$subfolder2/\\$label"} ; + + } + else + { + # Remove surrounding quotes if any, to add them again in case of space + $label = join( q{}, quotewords('\s+', 0, $label ) ) ; + if ( $label =~ m{ } ) + { + push @labels_subfolder2, qq{"$subfolder2/$label"} ; + } + else + { + push @labels_subfolder2, "$subfolder2/$label" ; + } + } + } + + my $labels_subfolder2 = join( ' ', sort @labels_subfolder2 ) ; + + return $labels_subfolder2 ; +} + +sub tests_labels +{ + note( 'Entering tests_labels()' ) ; + + is( undef, labels( ), 'labels: no parameters => undef' ) ; + is( undef, labels( undef ), 'labels: undef => undef' ) ; + require_ok( "Test::MockObject" ) ; + my $myimap = Test::MockObject->new( ) ; + + $myimap->mock( 'fetch_hash', + sub { + return( + { '1' => { + 'X-GM-LABELS' => '\Seen Blabla' + } + } + ) ; + } + ) ; + $myimap->mock( 'Debug' , sub { } ) ; + $myimap->mock( 'Unescape', sub { return Mail::IMAPClient::Unescape( @_ ) } ) ; # real one + + is( undef, labels( $myimap ), 'labels: one parameter => undef' ) ; + is( '\Seen Blabla', labels( $myimap, '1' ), 'labels: $mysync UID_1 => \Seen Blabla' ) ; + + note( 'Leaving tests_labels()' ) ; + return ; +} + +sub labels +{ + my ( $myimap, $uid ) = @ARG ; + + if ( not all_defined( $myimap, $uid ) ) { + return ; + } + + my $hash = $myimap->fetch_hash( [ $uid ], 'X-GM-LABELS' ) ; + + my $labels = $hash->{ $uid }->{ 'X-GM-LABELS' } ; + #$labels = $myimap->Unescape( $labels ) ; + return $labels ; +} + +sub tests_synclabels +{ + note( 'Entering tests_synclabels()' ) ; + + is( undef, synclabels( ), 'synclabels: no parameters => undef' ) ; + is( undef, synclabels( undef ), 'synclabels: undef => undef' ) ; + my $mysync ; + is( undef, synclabels( $mysync ), 'synclabels: var undef => undef' ) ; + + require_ok( "Test::MockObject" ) ; + $mysync = {} ; + + my $myimap1 = Test::MockObject->new( ) ; + $myimap1->mock( 'fetch_hash', + sub { + return( + { '1' => { + 'X-GM-LABELS' => '\Seen Blabla' + } + } + ) ; + } + ) ; + $myimap1->mock( 'Debug', sub { } ) ; + $myimap1->mock( 'Unescape', sub { return Mail::IMAPClient::Unescape( @_ ) } ) ; # real one + + my $myimap2 = Test::MockObject->new( ) ; + + $myimap2->mock( 'store', + sub { + return 1 ; + } + ) ; + + + $mysync->{imap1} = $myimap1 ; + $mysync->{imap2} = $myimap2 ; + + is( undef, synclabels( $mysync ), 'synclabels: fresh $mysync => undef' ) ; + + is( undef, synclabels( $mysync, '1' ), 'synclabels: $mysync UID_1 alone => undef' ) ; + is( 1, synclabels( $mysync, '1', '2' ), 'synclabels: $mysync UID_1 UID_2 => 1' ) ; + + note( 'Leaving tests_synclabels()' ) ; + return ; +} + + +sub synclabels +{ + my( $mysync, $uid1, $uid2 ) = @ARG ; + + if ( not all_defined( $mysync, $uid1, $uid2 ) ) { + return ; + } + my $myimap1 = $mysync->{ 'imap1' } || return ; + my $myimap2 = $mysync->{ 'imap2' } || return ; + + $mysync->{debuglabels} and $myimap1->Debug( 1 ) ; + my $labels1 = labels( $myimap1, $uid1 ) ; + $mysync->{debuglabels} and $myimap1->Debug( 0 ) ; + $mysync->{debuglabels} and myprint( "Host1 labels: $labels1\n" ) ; + + + + if ( $mysync->{ subfolder1 } and $labels1 ) + { + $labels1 = labels_remove_subfolder1( $labels1, $mysync->{ subfolder1 } ) ; + $mysync->{debuglabels} and myprint( "Host1 labels with subfolder1: $labels1\n" ) ; + } + + if ( $mysync->{ subfolder2 } and $labels1 ) + { + $labels1 = labels_add_subfolder2( $labels1, $mysync->{ subfolder2 } ) ; + $mysync->{debuglabels} and myprint( "Host1 labels with subfolder2: $labels1\n" ) ; + } + + my $store ; + if ( $labels1 and not $mysync->{ dry } ) + { + $mysync->{ debuglabels } and $myimap2->Debug( 1 ) ; + $store = $myimap2->store( $uid2, "X-GM-LABELS ($labels1)" ) ; + $mysync->{ debuglabels } and $myimap2->Debug( 0 ) ; + } + return $store ; +} + + +sub tests_resynclabels +{ + note( 'Entering tests_resynclabels()' ) ; + + is( undef, resynclabels( ), 'resynclabels: no parameters => undef' ) ; + is( undef, resynclabels( undef ), 'resynclabels: undef => undef' ) ; + my $mysync ; + is( undef, resynclabels( $mysync ), 'resynclabels: var undef => undef' ) ; + + my ( $h1_fir_ref, $h2_fir_ref ) ; + + $mysync->{ debuglabels } = 1 ; + $h1_fir_ref->{ 11 }->{ 'X-GM-LABELS' } = '\Seen Baa Kii' ; + $h2_fir_ref->{ 22 }->{ 'X-GM-LABELS' } = '\Seen Baa Kii' ; + + # labels are equal + is( 1, resynclabels( $mysync, 11, 22, $h1_fir_ref, $h2_fir_ref ), + 'resynclabels: $mysync UID_1 UID_2 labels are equal => 1' ) ; + + # labels are different + $h2_fir_ref->{ 22 }->{ 'X-GM-LABELS' } = '\Seen Zuu' ; + require_ok( "Test::MockObject" ) ; + my $myimap2 = Test::MockObject->new( ) ; + $myimap2->mock( 'store', + sub { + return 1 ; + } + ) ; + $myimap2->mock( 'Debug', sub { } ) ; + $mysync->{imap2} = $myimap2 ; + + is( 1, resynclabels( $mysync, 11, 22, $h1_fir_ref, $h2_fir_ref ), + 'resynclabels: $mysync UID_1 UID_2 labels are not equal => store => 1' ) ; + + note( 'Leaving tests_resynclabels()' ) ; + return ; +} + + + +sub resynclabels +{ + my( $mysync, $uid1, $uid2, $h1_fir_ref, $h2_fir_ref, $h1_folder ) = @ARG ; + + if ( not all_defined( $mysync, $uid1, $uid2, $h1_fir_ref, $h2_fir_ref ) ) { + return ; + } + + my $labels1 = $h1_fir_ref->{ $uid1 }->{ 'X-GM-LABELS' } || q{} ; + my $labels2 = $h2_fir_ref->{ $uid2 }->{ 'X-GM-LABELS' } || q{} ; + + if ( $mysync->{ subfolder1 } and $labels1 ) + { + $labels1 = labels_remove_subfolder1( $labels1, $mysync->{ subfolder1 } ) ; + } + + if ( $mysync->{ subfolder2 } and $labels1 ) + { + $labels1 = labels_add_subfolder2( $labels1, $mysync->{ subfolder2 }, $h1_folder ) ; + $labels2 = labels_remove_special( $labels2 ) ; + } + $mysync->{ debuglabels } and myprint( "Host1 labels fixed: $labels1\n" ) ; + $mysync->{ debuglabels } and myprint( "Host2 labels : $labels2\n" ) ; + + my $store ; + if ( $labels1 eq $labels2 ) + { + # no sync needed + $mysync->{ debuglabels } and myprint( "Labels are already equal\n" ) ; + return 1 ; + } + elsif ( not $mysync->{ dry } ) + { + # sync needed + $mysync->{debuglabels} and $mysync->{imap2}->Debug( 1 ) ; + $store = $mysync->{imap2}->store( $uid2, "X-GM-LABELS ($labels1)" ) ; + $mysync->{debuglabels} and $mysync->{imap2}->Debug( 0 ) ; + } + + return $store ; +} + +sub tests_uniq +{ + note( 'Entering tests_uniq()' ) ; + + is( 0, uniq( ), 'uniq: undef => 0' ) ; + is_deeply( [ 'one' ], [ uniq( 'one' ) ], 'uniq: one => one' ) ; + is_deeply( [ 'one' ], [ uniq( 'one', 'one' ) ], 'uniq: one one => one' ) ; + is_deeply( [ 'one', 'two' ], [ uniq( 'one', 'one', 'two', 'one', 'two' ) ], 'uniq: one one two one two => one two' ) ; + note( 'Leaving tests_uniq()' ) ; + return ; +} + +sub uniq +{ + my @list = @ARG ; + my %seen = ( ) ; + my @uniq = ( ) ; + foreach my $item ( @list ) { + if ( ! $seen{ $item } ) { + $seen{ $item } = 1 ; + push( @uniq, $item ) ; + } + } + return @uniq ; +} + + +sub length_ref +{ + my $string_ref = shift ; + my $string_len = defined ${ $string_ref } ? length( ${ $string_ref } ) : q{} ; # length or empty string + return $string_len ; +} + +sub tests_length_ref +{ + note( 'Entering tests_length_ref()' ) ; + + my $notdefined ; + is( q{}, length_ref( \$notdefined ), q{length_ref: value not defined} ) ; + my $notref ; + is( q{}, length_ref( $notref ), q{length_ref: param not a ref} ) ; + + my $lala = 'lala' ; + is( 4, length_ref( \$lala ), q{length_ref: lala length == 4} ) ; + is( 4, length_ref( \'lili' ), q{length_ref: lili length == 4} ) ; + + note( 'Leaving tests_length_ref()' ) ; + return ; +} + +sub date_for_host2 +{ + my( $h1_msg, $h1_idate ) = @_ ; + + my $h1_date = q{} ; + + if ( $syncinternaldates ) { + $h1_date = $h1_idate ; + $sync->{ debug } and myprint( "internal date from host1: [$h1_date]\n" ) ; + $h1_date = good_date( $h1_date ) ; + $sync->{ debug } and myprint( "internal date from host1: [$h1_date] (fixed)\n" ) ; + } + + if ( $idatefromheader ) { + $h1_date = $sync->{imap1}->get_header( $h1_msg, 'Date' ) ; + $sync->{ debug } and myprint( "header date from host1: [$h1_date]\n" ) ; + $h1_date = good_date( $h1_date ) ; + $sync->{ debug } and myprint( "header date from host1: [$h1_date] (fixed)\n" ) ; + } + + return( $h1_date ) ; +} + + +sub subject +{ + my $string = shift ; + my $subject = q{} ; + + my $header = extract_header( $string ) ; + + if( $header =~ m/^Subject:[ \t]*([^\n\r]*)\r?$/msx ) { + #myprint( "MMM[$1]\n" ) ; + $subject = $1 ; + } + return( $subject ) ; +} + +sub tests_subject +{ + note( 'Entering tests_subject()' ) ; + + ok( q{} eq subject( q{} ), 'subject: null') ; + is( '', subject( 'Subject:' ), 'Subject:') ; + is( '', subject( "Subject:\r\n" ), 'Subject:\r\n') ; + ok( 'toto le hero' eq subject( 'Subject: toto le hero' ), 'Subject: toto le hero') ; + ok( 'toto le hero' eq subject( 'Subject:toto le hero' ), 'Subject:toto le hero') ; + ok( 'toto le hero' eq subject( "Subject:toto le hero\r\n" ), 'Subject: toto le hero\r\n') ; + + my $MESS ; + $MESS = <<'EOF'; +From: lalala +Subject: toto le hero +Date: zzzzzz + +Boogie boogie +EOF + ok( 'toto le hero' eq subject( $MESS ), 'subject: toto le hero 2') ; + + $MESS = <<'EOF'; +Subject: toto le hero +From: lalala +Date: zzzzzz + +Boogie boogie +EOF + ok( 'toto le hero' eq subject( $MESS ), 'subject: toto le hero 3') ; + + + $MESS = <<'EOF'; +From: lalala +Subject: cuicui +Date: zzzzzz + +Subject: toto le hero +EOF + ok( 'cuicui' eq subject( $MESS ), 'subject: cuicui') ; + + $MESS = <<'EOF'; +From: lalala +Date: zzzzzz + +Subject: toto le hero +EOF + ok( q{} eq subject( $MESS ), 'subject: null but body could') ; + + + $MESS = <<'EOF'; +From: lalala +Subject: +Date: zzzzzz + +Subject: toto le hero +EOF + is( '', subject( $MESS ), 'Subject:') ; + + + + note( 'Leaving tests_subject()' ) ; + return ; +} + + +# GlobVar +# $h2_uidguess +# ... +# +# +sub append_message_on_host2 +{ + my( $mysync, $string_ref, $h1_fold, $h1_msg, $string_len, $h2_fold, $h1_size, $h1_flags, $h1_date, $cache_dir ) = @_ ; + myprint( debugmemory( $mysync, " at A1" ) ) ; + + my $new_id ; + if ( ! $mysync->{dry} ) { + $new_id = $mysync->{imap2}->append_string( $h2_fold, ${ $string_ref }, $h1_flags, $h1_date ) ; + myprint( debugmemory( $mysync, " at A2" ) ) ; + if ( ! defined $new_id ){ + my $subject = subject( ${ $string_ref } ) ; + my $error_imap = $mysync->{imap2}->LastError || q{} ; + my $error = "- msg $h1_fold/$h1_msg {$string_len} could not append ( Subject:[$subject], Date:[$h1_date], Size:[$h1_size], Flags:[$h1_flags] ) to folder $h2_fold: $error_imap\n" ; + errors_incr( $mysync, $error ) ; + $mysync->{ h1_nb_msg_processed } +=1 ; + return ; + } + else{ + # good + # $new_id is an id if the IMAP server has the + # UIDPLUS capability else just a ref + if ( $new_id !~ m{^\d+$}x ) { + $new_id = lastuid( $mysync->{imap2}, $h2_fold, $h2_uidguess ) ; + } + if ( $mysync->{ synclabels } ) { synclabels( $mysync, $h1_msg, $new_id ) } + $h2_uidguess += 1 ; + $mysync->{ total_bytes_transferred } += $string_len ; + $mysync->{ nb_msg_transferred } += 1 ; + $mysync->{ h1_nb_msg_processed } +=1 ; + $mysync->{ biggest_message_transferred } = max( $string_len, $mysync->{ biggest_message_transferred } ) ; + + my $time_spent = timesince( $mysync->{begin_transfer_time} ) ; + my $rate = bytes_display_string_bin( $mysync->{total_bytes_transferred} / $time_spent ) ; + my $eta = eta( $mysync ) ; + my $amount_transferred = bytes_display_string_bin( $mysync->{total_bytes_transferred} ) ; + myprintf( "msg %s/%-19s copied to %s/%-10s %.2f msgs/s %s/s %s copied %s\n", + $h1_fold, "$h1_msg {$string_len}", $h2_fold, $new_id, $mysync->{nb_msg_transferred}/$time_spent, $rate, + $amount_transferred, + $eta ); + sleep_if_needed( $mysync ) ; + if ( $usecache and $cacheaftercopy and $new_id =~ m{^\d+$}x ) { + $debugcache and myprint( "touch $cache_dir/${h1_msg}_$new_id\n" ) ; + touch( "$cache_dir/${h1_msg}_$new_id" ) + or croak( "Couldn't touch $cache_dir/${h1_msg}_$new_id" ) ; + } + if ( $mysync->{ delete1 } ) { + delete_message_on_host1( $mysync, $h1_fold, $mysync->{ expungeaftereach }, $h1_msg ) ; + } + #myprint( "PRESS ENTER" ) and my $a = <> ; + + return( $new_id ) ; + } + } + else{ + $nb_msg_skipped_dry_mode += 1 ; + $mysync->{ h1_nb_msg_processed } += 1 ; + } + + return ; +} + + +sub tests_sleep_if_needed +{ + note( 'Entering tests_sleep_if_needed()' ) ; + + is( undef, sleep_if_needed( ), 'sleep_if_needed: no args => undef' ) ; + my $mysync ; + is( undef, sleep_if_needed( $mysync ), 'sleep_if_needed: arg undef => undef' ) ; + + $mysync->{maxbytespersecond} = 1000 ; + is( 0, sleep_if_needed( $mysync ), 'sleep_if_needed: maxbytespersecond only => no sleep => 0' ) ; + $mysync->{begin_transfer_time} = time ; # now + is( 0, sleep_if_needed( $mysync ), 'sleep_if_needed: begin_transfer_time now => no sleep => 0' ) ; + $mysync->{begin_transfer_time} = time - 2 ; # 2 s before + is( 0, sleep_if_needed( $mysync ), 'sleep_if_needed: total_bytes_transferred == 0 => no sleep => 0' ) ; + + $mysync->{total_bytes_transferred} = 2200 ; + $mysync->{begin_transfer_time} = time - 2 ; # 2 s before + is( '0.20', sleep_if_needed( $mysync ), 'sleep_if_needed: total_bytes_transferred == 2200 since 2s => sleep 0.2s' ) ; + is( '0', sleep_if_needed( $mysync ), 'sleep_if_needed: total_bytes_transferred == 2200 since 2+2 == 4s => no sleep' ) ; + + $mysync->{maxsleep} = 0.1 ; + $mysync->{begin_transfer_time} = time - 2 ; # 2 s before again + is( '0.10', sleep_if_needed( $mysync ), 'sleep_if_needed: total_bytes_transferred == 4000 since 2s but maxsleep 0.1s => sleep 0.1s' ) ; + + $mysync->{maxbytesafter} = 4000 ; + $mysync->{begin_transfer_time} = time - 2 ; # 2 s before again + is( 0, sleep_if_needed( $mysync ), 'sleep_if_needed: maxbytesafter == total_bytes_transferred => no sleep => 0' ) ; + + note( 'Leaving tests_sleep_if_needed()' ) ; + return ; +} + + +sub sleep_if_needed +{ + my( $mysync ) = shift ; + + if ( ! $mysync ) { + return ; + } + # No need to go further if there is no limit set + if ( + not ( + $mysync->{maxmessagespersecond} + or $mysync->{maxbytespersecond} + ) + ) { + return ; + } + + $mysync->{maxsleep} = defined $mysync->{maxsleep} ? $mysync->{maxsleep} : $MAX_SLEEP ; + # Must be positive + $mysync->{maxsleep} = max( 0, $mysync->{maxsleep} ) ; + + my $time_spent = timesince( $mysync->{begin_transfer_time} ) ; + my $sleep_max_messages = sleep_max_messages( $mysync->{nb_msg_transferred}, $time_spent, $mysync->{maxmessagespersecond} ) ; + + my $maxbytesafter = $mysync->{maxbytesafter} || 0 ; + my $total_bytes_transferred = $mysync->{total_bytes_transferred} || 0 ; + my $total_bytes_to_consider = $total_bytes_transferred - $maxbytesafter ; + + #myprint( "maxbytesafter:$maxbytesafter\n" ) ; + #myprint( "total_bytes_to_consider:$total_bytes_to_consider\n" ) ; + + my $sleep_max_bytes = sleep_max_bytes( $total_bytes_to_consider, $time_spent, $mysync->{maxbytespersecond} ) ; + my $sleep_max = min( $mysync->{maxsleep}, max( $sleep_max_messages, $sleep_max_bytes ) ) ; + $sleep_max = mysprintf( "%.2f", $sleep_max ) ; # round with 2 decimals. + if ( $sleep_max > 0 ) { + myprint( "sleeping $sleep_max s\n" ) ; + sleep $sleep_max ; + # Slept + return $sleep_max ; + } + # No sleep + return 0 ; +} + +sub sleep_max_messages +{ + # how long we have to sleep to go under max_messages_per_second + my( $nb_msg_transferred, $time_spent, $maxmessagespersecond ) = @_ ; + if ( ( not defined $maxmessagespersecond ) or $maxmessagespersecond <= 0 ) { return( 0 ) } ; + my $sleep = ( $nb_msg_transferred / $maxmessagespersecond ) - $time_spent ; + # the sleep must be positive + return( max( 0, $sleep ) ) ; +} + + +sub tests_sleep_max_messages +{ + note( 'Entering tests_sleep_max_messages()' ) ; + + ok( 0 == sleep_max_messages( 4, 2, undef ), 'sleep_max_messages: maxmessagespersecond = undef') ; + ok( 0 == sleep_max_messages( 4, 2, 0 ), 'sleep_max_messages: maxmessagespersecond = 0') ; + ok( 0 == sleep_max_messages( 4, 2, $MINUS_ONE ), 'sleep_max_messages: maxmessagespersecond = -1') ; + ok( 0 == sleep_max_messages( 4, 2, 2 ), 'sleep_max_messages: maxmessagespersecond = 2 max reached') ; + ok( 2 == sleep_max_messages( 8, 2, 2 ), 'sleep_max_messages: maxmessagespersecond = 2 max over') ; + ok( 0 == sleep_max_messages( 2, 2, 2 ), 'sleep_max_messages: maxmessagespersecond = 2 max not reached') ; + + note( 'Leaving tests_sleep_max_messages()' ) ; + return ; +} + + +sub sleep_max_bytes +{ + # how long we have to sleep to go under max_bytes_per_second + my( $total_bytes_to_consider, $time_spent, $maxbytespersecond ) = @_ ; + $total_bytes_to_consider ||= 0 ; + $time_spent ||= 0 ; + + if ( ( not defined $maxbytespersecond ) or $maxbytespersecond <= 0 ) { return( 0 ) } ; + #myprint( "total_bytes_to_consider:$total_bytes_to_consider\n" ) ; + my $sleep = ( $total_bytes_to_consider / $maxbytespersecond ) - $time_spent ; + # the sleep must be positive + return( max( 0, $sleep ) ) ; +} + + +sub tests_sleep_max_bytes +{ + note( 'Entering tests_sleep_max_bytes()' ) ; + + ok( 0 == sleep_max_bytes( 4000, 2, undef ), 'sleep_max_bytes: maxbytespersecond == undef => sleep 0' ) ; + ok( 0 == sleep_max_bytes( 4000, 2, 0 ), 'sleep_max_bytes: maxbytespersecond = 0 => sleep 0') ; + ok( 0 == sleep_max_bytes( 4000, 2, $MINUS_ONE ), 'sleep_max_bytes: maxbytespersecond = -1 => sleep 0') ; + ok( 0 == sleep_max_bytes( 4000, 2, 2000 ), 'sleep_max_bytes: maxbytespersecond = 2k max reached sharp => sleep 0') ; + ok( 2 == sleep_max_bytes( 8000, 2, 2000 ), 'sleep_max_bytes: maxbytespersecond = 2k max over => sleep a little') ; + ok( 0 == sleep_max_bytes( -8000, 2, 2000 ), 'sleep_max_bytes: maxbytespersecond = 2k max not reached => sleep 0') ; + ok( 0 == sleep_max_bytes( 2000, 2, 2000 ), 'sleep_max_bytes: maxbytespersecond = 2k max not reached => sleep 0') ; + ok( 0 == sleep_max_bytes( -2000, 2, 1000 ), 'sleep_max_bytes: maxbytespersecond = 1k max not reached => sleep 0') ; + + note( 'Leaving tests_sleep_max_bytes()' ) ; + return ; +} + + +sub delete_message_on_host1 +{ + my( $mysync, $h1_fold, $expunge, @h1_msg ) = @_ ; + if ( ! $mysync->{ delete1 } ) { return ; } + if ( ! @h1_msg ) { return ; } + delete_messages_on_any( + $mysync, + $mysync->{ acc1 }, + $mysync->{ imap1 }, + "Host1: $h1_fold", + $expunge, + $split1, + @h1_msg ) ; + return ; +} + +sub tests_operators_and_exclam_precedence +{ + note( 'Entering tests_operators_and_exclam_precedence()' ) ; + + is( 1, ! 0, 'tests_operators_and_exclam_precedence: ! 0 => 1' ) ; + is( "", ! 1, 'tests_operators_and_exclam_precedence: ! 1 => ""' ) ; + is( 1, not( 0 ), 'tests_operators_and_exclam_precedence: not( 0 ) => 1' ) ; + is( "", not( 1 ), 'tests_operators_and_exclam_precedence: not( 1 ) => ""' ) ; + + # I wrote those tests to avoid perlcrit "Mixed high and low-precedence booleans" + # and change sub delete_messages_on_any() but got 4 more warnings... So now commented. + + #is( 0, ( ! 0 and 0 ), 'tests_operators_and_exclam_precedence: ! 0 and 0 ) => 0' ) ; + #is( 1, ( ! 0 and 1 ), 'tests_operators_and_exclam_precedence: ! 0 and 1 ) => 1' ) ; + #is( "", ( ! 1 and 0 ), 'tests_operators_and_exclam_precedence: ! 1 and 0 ) => ""' ) ; + #is( "", ( ! 1 and 1 ), 'tests_operators_and_exclam_precedence: ! 1 and 1 ) => ""' ) ; + + is( 0, ( ! 0 && 0 ), 'tests_operators_and_exclam_precedence: ! 0 && 0 ) => 0' ) ; + is( 1, ( ! 0 && 1 ), 'tests_operators_and_exclam_precedence: ! 0 && 1 ) => 1' ) ; + is( "", ( ! 1 && 0 ), 'tests_operators_and_exclam_precedence: ! 1 && 0 ) => ""' ) ; + is( "", ( ! 1 && 1 ), 'tests_operators_and_exclam_precedence: ! 1 && 1 ) => ""' ) ; + + is( 2, ( ! 0 && 2 ), 'tests_operators_and_exclam_precedence: ! 0 && 2 ) => 1' ) ; + + note( 'Leaving tests_operators_and_exclam_precedence()' ) ; + return ; +} + + +sub delete_messages_on_any +{ + # $acc is not used yet, + # + my( $mysync, $acc, $imap, $hostX_folder, $expunge, $split, @messages ) = @_ ; + my $expunge_message = q{} ; + + my $dry_message = $mysync->{ dry_message } ; + $expunge_message = 'and expunged' if ( $expunge ) ; + # "Host1: msg " + + # $imap->Debug( 1 ) ; + + my @messages_to_mark_deleted = @messages ; + while ( my @messages_part = splice @messages_to_mark_deleted, 0, $split ) + { + foreach my $message ( @messages_part ) + { + myprint( "$hostX_folder/$message marking deleted $expunge_message $dry_message\n" ) ; + } + if ( ! $mysync->{dry} && @messages_part ) + { + my $nb_deleted = $imap->delete_message( $imap->Range( @messages_part ) ) ; + if ( defined $nb_deleted ) + { + # $nb_deleted is not accurate + $acc->{ nb_msg_deleted } += scalar @messages_part ; + } + else + { + my $error_imap = $imap->LastError || q{} ; + my $error = join( q{}, "$hostX_folder folder, could not delete ", + scalar @messages_part, ' messages: ', $error_imap, "\n" ) ; + errors_incr( $mysync, $error ) ; + } + } + } + + if ( $expunge ) { + uidexpunge_or_expunge( $mysync, $imap, @messages ) ; + } + + #$imap->Debug( 0 ) ; + + return ; +} + + +sub tests_uidexpunge_or_expunge +{ + note( 'Entering tests_uidexpunge_or_expunge()' ) ; + + + is( undef, uidexpunge_or_expunge( ), 'uidexpunge_or_expunge: no args => undef' ) ; + my $mysync ; + is( undef, uidexpunge_or_expunge( $mysync ), 'uidexpunge_or_expunge: undef args => undef' ) ; + $mysync = {} ; + is( undef, uidexpunge_or_expunge( $mysync ), 'uidexpunge_or_expunge: arg empty => undef' ) ; + my $imap ; + is( undef, uidexpunge_or_expunge( $mysync, $imap ), 'uidexpunge_or_expunge: undef Mail-IMAPClient instance => undef' ) ; + + require_ok( "Test::MockObject" ) ; + $imap = Test::MockObject->new( ) ; + is( undef, uidexpunge_or_expunge( $mysync, $imap ), 'uidexpunge_or_expunge: no message (1) to uidexpunge => undef' ) ; + + my @messages = ( ) ; + is( undef, uidexpunge_or_expunge( $mysync, $imap, @messages ), 'uidexpunge_or_expunge: no message (2) to uidexpunge => undef' ) ; + + @messages = ( '2', '1' ) ; + $imap->mock( 'uidexpunge', sub { return ; } ) ; + $imap->mock( 'expunge', sub { return ; } ) ; + is( undef, uidexpunge_or_expunge( $mysync, $imap, @messages ), 'uidexpunge_or_expunge: uidexpunge failure => expunge failure => undef' ) ; + + $imap->mock( 'expunge', sub { return 1 ; } ) ; + is( 1, uidexpunge_or_expunge( $mysync, $imap, @messages ), 'uidexpunge_or_expunge: uidexpunge failure => expunge ok => 1' ) ; + + $imap->mock( 'uidexpunge', sub { return 1 ; } ) ; + is( 1, uidexpunge_or_expunge( $mysync, $imap, @messages ), 'uidexpunge_or_expunge: messages to uidexpunge ok => 1' ) ; + + note( 'Leaving tests_uidexpunge_or_expunge()' ) ; + return ; +} + +sub uidexpunge_or_expunge +{ + my $mysync = shift ; + my $imap = shift ; + my @messages = @ARG ; + + if ( ! $imap ) { return ; } ; + if ( ! @messages ) { return ; } ; + + # Doing uidexpunge + my @uidexpunge_result = $imap->uidexpunge( @messages ) ; + if ( @uidexpunge_result ) { + return 1 ; + } + # Failure so doing expunge + my $expunge_result = $imap->expunge( ) ; + if ( $expunge_result ) { + return 1 ; + } + # bad trip + return ; +} + +sub eta_print +{ + my $mysync = shift ; + if ( my $eta = eta( $mysync ) ) + { + myprint( "$eta\n" ) ; + } + return ; +} + +sub tests_eta +{ + note( 'Entering tests_eta()' ) ; + + is( q{}, eta( ), 'eta: no args => ""' ) ; + is( q{}, eta( undef ), 'eta: undef => ""' ) ; + my $mysync = {} ; + # No foldersizes + is( q{}, eta( $mysync ), 'eta: No foldersizes => ""' ) ; + + $mysync->{ foldersizes } = 1 ; + + $mysync->{ begin_transfer_time } = time ; # Now + $mysync->{ h1_nb_msg_processed } = 0 ; + + is( "ETA: " . localtimez( time ) . " 0 s 0/0 msgs left", + eta( $mysync ), + 'eta: no args => ETA: "Now" 0 s 0/0 msgs left' ) ; + + $mysync->{ h1_nb_msg_processed } = 1 ; + $mysync->{ h1_nb_msg_start } = 2 ; + is( "ETA: " . localtimez( time ) . " 0 s 1/2 msgs left", + eta( $mysync ), + 'eta: 1, 1, 2 => ETA: "Now" 0 s 1/2 msgs left' ) ; + + note( 'Leaving tests_eta()' ) ; + return ; +} + + +sub eta +{ + my( $mysync ) = shift ; + + if ( ! $mysync ) + { + return q{} ; + } + + return( q{} ) if not $mysync->{ foldersizes } ; + + my $h1_nb_msg_start = $mysync->{ h1_nb_msg_start } ; + my $h1_nb_processed = $mysync->{ h1_nb_msg_processed } ; + my $nb_msg_transferred = ( $mysync->{dry} ) ? $mysync->{ h1_nb_msg_processed } : $mysync->{ nb_msg_transferred } ; + my $time_spent = timesince( $mysync->{ begin_transfer_time } ) ; + $h1_nb_processed ||= 0 ; + $h1_nb_msg_start ||= 0 ; + $time_spent ||= 0 ; + + my $time_remaining = time_remaining( $time_spent, $h1_nb_processed, $h1_nb_msg_start, $nb_msg_transferred ) ; + $mysync->{ debug } and myprint( "time_spent: $time_spent time_remaining: $time_remaining\n" ) ; + my $nb_msg_remaining = $h1_nb_msg_start - $h1_nb_processed ; + my $eta_date = localtimez( time + $time_remaining ) ; + return( mysprintf( 'ETA: %s %1.0f s %s/%s msgs left', + $eta_date, $time_remaining, $nb_msg_remaining, $h1_nb_msg_start ) ) ; +} + + + + +sub time_remaining +{ + + my( $my_time_spent, $h1_nb_processed, $h1_nb_msg_start, $nb_transferred ) = @_ ; + + $nb_transferred ||= 1 ; # At least one is done (no division by zero) + $h1_nb_processed ||= 0 ; + $h1_nb_msg_start ||= $h1_nb_processed ; + $my_time_spent ||= 0 ; + + my $time_remaining = ( $my_time_spent / $nb_transferred ) * ( $h1_nb_msg_start - $h1_nb_processed ) ; + return( $time_remaining ) ; +} + + +sub tests_time_remaining +{ + note( 'Entering tests_time_remaining()' ) ; + + # time_spent, nb_processed, nb_to_do_total, nb_transferred + is( 0, time_remaining( ), 'time_remaining: no args -> 0' ) ; + is( 0, time_remaining( 0, 0, 0, 0 ), 'time_remaining: 0, 0, 0, 0 -> 0' ) ; + is( 1, time_remaining( 1, 1, 2, 1 ), 'time_remaining: 1, 1, 2, 1 -> 1' ) ; + is( 1, time_remaining( 9, 9, 10, 9 ), 'time_remaining: 9, 9, 10, 9 -> 1' ) ; + is( 9, time_remaining( 1, 1, 10, 1 ), 'time_remaining: 1, 1, 10, 1 -> 9' ) ; + is( 5, time_remaining( 5, 5, 10, 5 ), 'time_remaining: 5, 5, 10, 5 -> 5' ) ; + is( 25, time_remaining( 5, 5, 10, 0 ), 'time_remaining: 5, 5, 10, 0 -> ( 5 / 1 ) * ( 10 - 5) = 25' ) ; + is( 25, time_remaining( 5, 5, 10, 1 ), 'time_remaining: 5, 5, 10, 1 -> ( 5 / 1 ) * ( 10 - 5) = 25' ) ; + + note( 'Leaving tests_time_remaining()' ) ; + return ; +} + + +sub cache_map +{ + my ( $cache_files_ref, $h1_msgs_ref, $h2_msgs_ref ) = @_; + my ( %map1_2, %map2_1, %done2 ) ; + + my $h1_msgs_hash_ref = { } ; + my $h2_msgs_hash_ref = { } ; + + @{ $h1_msgs_hash_ref }{ @{ $h1_msgs_ref } } = ( ) ; + @{ $h2_msgs_hash_ref }{ @{ $h2_msgs_ref } } = ( ) ; + + foreach my $file ( sort @{ $cache_files_ref } ) { + $debugcache and myprint( "C12: $file\n" ) ; + ( $uid1, $uid2 ) = match_a_cache_file( $file ) ; + + if ( exists( $h1_msgs_hash_ref->{ defined $uid1 ? $uid1 : q{} } ) + and exists( $h2_msgs_hash_ref->{ defined $uid2 ? $uid2 : q{} } ) ) { + # keep only the greatest uid2 + # 130_2301 and + # 130_231 => keep only 130 -> 2301 + + # keep only the greatest uid1 + # 1601_260 and + # 161_260 => keep only 1601 -> 260 + my $max_uid2 = max( $uid2, $map1_2{ $uid1 } || $MINUS_ONE ) ; + if ( exists $done2{ $max_uid2 } ) { + if ( $done2{ $max_uid2 } < $uid1 ) { + $map1_2{ $uid1 } = $max_uid2 ; + delete $map1_2{ $done2{ $max_uid2 } } ; + $done2{ $max_uid2 } = $uid1 ; + } + }else{ + $map1_2{ $uid1 } = $max_uid2 ; + $done2{ $max_uid2 } = $uid1 ; + } + }; + + } + %map2_1 = reverse %map1_2 ; + return( \%map1_2, \%map2_1) ; +} + +sub tests_cache_map +{ + note( 'Entering tests_cache_map()' ) ; + + #$debugcache = 1 ; + my @cache_files = qw ( + 100_200 + 101_201 + 120_220 + 142_242 + 143_243 + 177_277 + 177_278 + 177_279 + 155_255 + 180_280 + 181_280 + 182_280 + 130_231 + 130_2301 + 161_260 + 1601_260 + ) ; + + my $msgs_1 = [120, 142, 143, 144, 161, 1601, 177, 182, 130 ]; + my $msgs_2 = [ 242, 243, 260, 299, 377, 279, 255, 280, 231, 2301 ]; + + my( $c12, $c21 ) ; + ok( ( $c12, $c21 ) = cache_map( \@cache_files, $msgs_1, $msgs_2 ), 'cache_map: 02' ); + my $a1 = [ sort { $a <=> $b } keys %{ $c12 } ] ; + my $a2 = [ sort { $a <=> $b } keys %{ $c21 } ] ; + ok( 0 == compare_lists( [ 130, 142, 143, 177, 182, 1601 ], $a1 ), 'cache_map: 03' ); + ok( 0 == compare_lists( [ 242, 243, 260, 279, 280, 2301 ], $a2 ), 'cache_map: 04' ); + ok( ! $c12->{161}, 'cache_map: ! 161 -> 260' ); + ok( 260 == $c12->{1601}, 'cache_map: 1601 -> 260' ); + ok( 2301 == $c12->{130}, 'cache_map: 130 -> 2301' ); + #myprint( $c12->{1601}, "\n" ) ; + + note( 'Leaving tests_cache_map()' ) ; + return ; + +} + +sub cache_dir_fix +{ + my $cache_dir = shift ; + $cache_dir =~ s/([;<>\*\|`&\$!#\(\)\[\]\{\}:'"\\])/\\$1/xg ; + #myprint( "cache_dir_fix: $cache_dir\n" ) ; + return( $cache_dir ) ; +} + +sub tests_cache_dir_fix +{ + note( 'Entering tests_cache_dir_fix()' ) ; + + ok( 'lalala' eq cache_dir_fix('lalala'), 'cache_dir_fix: lalala -> lalala' ); + ok( 'ii\\\\ii' eq cache_dir_fix('ii\ii'), 'cache_dir_fix: ii\ii -> ii\\\\ii' ); + ok( 'ii@ii' eq cache_dir_fix('ii@ii'), 'cache_dir_fix: ii@ii -> ii@ii' ); + ok( 'ii@ii\\:ii' eq cache_dir_fix('ii@ii:ii'), 'cache_dir_fix: ii@ii:ii -> ii@ii\\:ii' ); + ok( 'i\\\\i\\\\ii' eq cache_dir_fix('i\i\ii'), 'cache_dir_fix: i\i\ii -> i\\\\i\\\\ii' ); + ok( 'i\\\\ii' eq cache_dir_fix('i\\ii'), 'cache_dir_fix: i\\ii -> i\\\\\\\\ii' ); + ok( '\\\\ ' eq cache_dir_fix('\\ '), 'cache_dir_fix: \\ -> \\\\\ ' ); + ok( '\\\\ ' eq cache_dir_fix('\ '), 'cache_dir_fix: \ -> \\\\\ ' ); + ok( '\[bracket\]' eq cache_dir_fix('[bracket]'), 'cache_dir_fix: [bracket] -> \[bracket\]' ); + + note( 'Leaving tests_cache_dir_fix()' ) ; + return ; +} + +sub cache_dir_fix_win +{ + my $cache_dir = shift ; + $cache_dir =~ s/(\[|\])/[$1]/xg ; + #myprint( "cache_dir_fix_win: $cache_dir\n" ) ; + return( $cache_dir ) ; +} + +sub tests_cache_dir_fix_win +{ + note( 'Entering tests_cache_dir_fix_win()' ) ; + + ok( 'lalala' eq cache_dir_fix_win('lalala'), 'cache_dir_fix_win: lalala -> lalala' ); + ok( '[[]bracket[]]' eq cache_dir_fix_win('[bracket]'), 'cache_dir_fix_win: [bracket] -> [[]bracket[]]' ); + + note( 'Leaving tests_cache_dir_fix_win()' ) ; + return ; +} + + + + +sub get_cache +{ + my ( $cache_dir, $h1_msgs_ref, $h2_msgs_ref, $h1_msgs_all_hash_ref, $h2_msgs_all_hash_ref ) = @_; + + $debugcache and myprint( "Entering get_cache\n" ) ; + + -d $cache_dir or return( undef ); # exit if cache directory doesn't exist + $debugcache and myprint( "cache_dir : $cache_dir\n" ) ; + + + if ( 'MSWin32' ne $OSNAME ) { + $cache_dir = cache_dir_fix( $cache_dir ) ; + }else{ + $cache_dir = cache_dir_fix_win( $cache_dir ) ; + } + + $debugcache and myprint( "cache_dir_fix: $cache_dir\n" ) ; + + my @cache_files = bsd_glob( "$cache_dir/*" ) ; + #$debugcache and myprint( "cache_files: [@cache_files]\n" ) ; + + $debugcache and myprint( 'cache_files: ', scalar @cache_files , " files found\n" ) ; + + my( $cache_1_2_ref, $cache_2_1_ref ) + = cache_map( \@cache_files, $h1_msgs_ref, $h2_msgs_ref ) ; + + clean_cache( \@cache_files, $cache_1_2_ref, $h1_msgs_all_hash_ref, $h2_msgs_all_hash_ref ) ; + + $debugcache and myprint( "Exiting get_cache\n" ) ; + return( $cache_1_2_ref, $cache_2_1_ref ) ; +} + + +sub tests_get_cache +{ + note( 'Entering tests_get_cache()' ) ; + + ok( not( get_cache('/cache_no_exist') ), 'get_cache: /cache_no_exist' ); + ok( ( not -d 'W/tmp/cache/F1/F2' or rmtree( 'W/tmp/cache/F1/F2' ) ), 'get_cache: rmtree W/tmp/cache/F1/F2' ) ; + ok( mkpath( 'W/tmp/cache/F1/F2' ), 'get_cache: mkpath W/tmp/cache/F1/F2' ) ; + + my @test_files_cache = ( qw( + W/tmp/cache/F1/F2/100_200 + W/tmp/cache/F1/F2/101_201 + W/tmp/cache/F1/F2/120_220 + W/tmp/cache/F1/F2/142_242 + W/tmp/cache/F1/F2/143_243 + W/tmp/cache/F1/F2/177_277 + W/tmp/cache/F1/F2/177_377 + W/tmp/cache/F1/F2/177_777 + W/tmp/cache/F1/F2/155_255 + ) ) ; + ok( touch( @test_files_cache ), 'get_cache: touch W/tmp/cache/F1/F2/...' ) ; + + + # on cache: 100_200 101_201 142_242 143_243 177_277 177_377 177_777 155_255 + # on live: + my $msgs_1 = [120, 142, 143, 144, 177 ]; + my $msgs_2 = [ 242, 243, 299, 377, 777, 255 ]; + + my $msgs_all_1 = { 120 => 0, 142 => 0, 143 => 0, 144 => 0, 177 => 0 } ; + my $msgs_all_2 = { 242 => 0, 243 => 0, 299 => 0, 377 => 0, 777 => 0, 255 => 0 } ; + + my( $c12, $c21 ) ; + ok( ( $c12, $c21 ) = get_cache( 'W/tmp/cache/F1/F2', $msgs_1, $msgs_2, $msgs_all_1, $msgs_all_2 ), 'get_cache: 02' ); + my $a1 = [ sort { $a <=> $b } keys %{ $c12 } ] ; + my $a2 = [ sort { $a <=> $b } keys %{ $c21 } ] ; + ok( 0 == compare_lists( [ 142, 143, 177 ], $a1 ), 'get_cache: 03' ); + ok( 0 == compare_lists( [ 242, 243, 777 ], $a2 ), 'get_cache: 04' ); + ok( -f 'W/tmp/cache/F1/F2/142_242', 'get_cache: file kept 142_242'); + ok( -f 'W/tmp/cache/F1/F2/142_242', 'get_cache: file kept 143_243'); + ok( ! -f 'W/tmp/cache/F1/F2/100_200', 'get_cache: file removed 100_200'); + ok( ! -f 'W/tmp/cache/F1/F2/101_201', 'get_cache: file removed 101_201'); + + # test clean_cache executed + $maxage = 2 ; + ok( touch(@test_files_cache), 'get_cache: touch W/tmp/cache/F1/F2/...' ) ; + ok( ( $c12, $c21 ) = get_cache('W/tmp/cache/F1/F2', $msgs_1, $msgs_2, $msgs_all_1, $msgs_all_2 ), 'get_cache: 02' ); + ok( -f 'W/tmp/cache/F1/F2/142_242', 'get_cache: file kept 142_242'); + ok( -f 'W/tmp/cache/F1/F2/142_242', 'get_cache: file kept 143_243'); + ok( ! -f 'W/tmp/cache/F1/F2/100_200', 'get_cache: file NOT removed 100_200'); + ok( ! -f 'W/tmp/cache/F1/F2/101_201', 'get_cache: file NOT removed 101_201'); + + + # strange files + #$debugcache = 1 ; + $maxage = undef ; + ok( ( not -d 'W/tmp/cache/rr\uee' or rmtree( 'W/tmp/cache/rr\uee' )), 'get_cache: rmtree W/tmp/cache/rr\uee' ) ; + ok( mkpath( 'W/tmp/cache/rr\uee' ), 'get_cache: mkpath W/tmp/cache/rr\uee' ) ; + + @test_files_cache = ( qw( + W/tmp/cache/rr\uee/100_200 + W/tmp/cache/rr\uee/101_201 + W/tmp/cache/rr\uee/120_220 + W/tmp/cache/rr\uee/142_242 + W/tmp/cache/rr\uee/143_243 + W/tmp/cache/rr\uee/177_277 + W/tmp/cache/rr\uee/177_377 + W/tmp/cache/rr\uee/177_777 + W/tmp/cache/rr\uee/155_255 + ) ) ; + ok( touch(@test_files_cache), 'get_cache: touch strange W/tmp/cache/...' ) ; + + # on cache: 100_200 101_201 142_242 143_243 177_277 177_377 177_777 155_255 + # on live: + $msgs_1 = [120, 142, 143, 144, 177 ] ; + $msgs_2 = [ 242, 243, 299, 377, 777, 255 ] ; + + $msgs_all_1 = { 120 => q{}, 142 => q{}, 143 => q{}, 144 => q{}, 177 => q{} } ; + $msgs_all_2 = { 242 => q{}, 243 => q{}, 299 => q{}, 377 => q{}, 777 => q{}, 255 => q{} } ; + + ok( ( $c12, $c21 ) = get_cache('W/tmp/cache/rr\uee', $msgs_1, $msgs_2, $msgs_all_1, $msgs_all_2), 'get_cache: strange path 02' ); + $a1 = [ sort { $a <=> $b } keys %{ $c12 } ] ; + $a2 = [ sort { $a <=> $b } keys %{ $c21 } ] ; + ok( 0 == compare_lists( [ 142, 143, 177 ], $a1 ), 'get_cache: strange path 03' ); + ok( 0 == compare_lists( [ 242, 243, 777 ], $a2 ), 'get_cache: strange path 04' ); + ok( -f 'W/tmp/cache/rr\uee/142_242', 'get_cache: strange path file kept 142_242'); + ok( -f 'W/tmp/cache/rr\uee/142_242', 'get_cache: strange path file kept 143_243'); + ok( ! -f 'W/tmp/cache/rr\uee/100_200', 'get_cache: strange path file removed 100_200'); + ok( ! -f 'W/tmp/cache/rr\uee/101_201', 'get_cache: strange path file removed 101_201'); + + note( 'Leaving tests_get_cache()' ) ; + return ; +} + +sub match_a_cache_file +{ + my $file = shift ; + my ( $cache_uid1, $cache_uid2 ) ; + + return( ( undef, undef ) ) if ( ! $file ) ; + if ( $file =~ m{(?:^|/)(\d+)_(\d+)$}x ) { + $cache_uid1 = $1 ; + $cache_uid2 = $2 ; + } + return( $cache_uid1, $cache_uid2 ) ; +} + +sub tests_match_a_cache_file +{ + note( 'Entering tests_match_a_cache_file()' ) ; + + my ( $tuid1, $tuid2 ) ; + ok( ( $tuid1, $tuid2 ) = match_a_cache_file( ), 'match_a_cache_file: no arg' ) ; + ok( ! defined $tuid1 , 'match_a_cache_file: no arg 1' ) ; + ok( ! defined $tuid2 , 'match_a_cache_file: no arg 2' ) ; + + ok( ( $tuid1, $tuid2 ) = match_a_cache_file( q{} ), 'match_a_cache_file: empty arg' ) ; + ok( ! defined $tuid1 , 'match_a_cache_file: empty arg 1' ) ; + ok( ! defined $tuid2 , 'match_a_cache_file: empty arg 2' ) ; + + ok( ( $tuid1, $tuid2 ) = match_a_cache_file( '000_000' ), 'match_a_cache_file: 000_000' ) ; + ok( '000' eq $tuid1, 'match_a_cache_file: 000_000 1' ) ; + ok( '000' eq $tuid2, 'match_a_cache_file: 000_000 2' ) ; + + ok( ( $tuid1, $tuid2 ) = match_a_cache_file( '123_456' ), 'match_a_cache_file: 123_456' ) ; + ok( '123' eq $tuid1, 'match_a_cache_file: 123_456 1' ) ; + ok( '456' eq $tuid2, 'match_a_cache_file: 123_456 2' ) ; + + ok( ( $tuid1, $tuid2 ) = match_a_cache_file( '/tmp/truc/123_456' ), 'match_a_cache_file: /tmp/truc/123_456' ) ; + ok( '123' eq $tuid1, 'match_a_cache_file: /tmp/truc/123_456 1' ) ; + ok( '456' eq $tuid2, 'match_a_cache_file: /tmp/truc/123_456 2' ) ; + + ok( ( $tuid1, $tuid2 ) = match_a_cache_file( '/lala123_456' ), 'match_a_cache_file: NO /lala123_456' ) ; + ok( ! $tuid1, 'match_a_cache_file: /lala123_456 1' ) ; + ok( ! $tuid2, 'match_a_cache_file: /lala123_456 2' ) ; + + ok( ( $tuid1, $tuid2 ) = match_a_cache_file( 'la123_456' ), 'match_a_cache_file: NO la123_456' ) ; + ok( ! $tuid1, 'match_a_cache_file: la123_456 1' ) ; + ok( ! $tuid2, 'match_a_cache_file: la123_456 2' ) ; + + note( 'Leaving tests_match_a_cache_file()' ) ; + return ; +} + +sub clean_cache +{ + my ( $cache_files_ref, $cache_1_2_ref, $h1_msgs_all_hash_ref, $h2_msgs_all_hash_ref ) = @_ ; + + $debugcache and myprint( "Entering clean_cache\n" ) ; + + $debugcache and myprint( map { "$_ -> " . $cache_1_2_ref->{ $_ } . "\n" } keys %{ $cache_1_2_ref } ) ; + foreach my $file ( @{ $cache_files_ref } ) { + $debugcache and myprint( "$file\n" ) ; + my ( $cache_uid1, $cache_uid2 ) = match_a_cache_file( $file ) ; + $debugcache and myprint( "u1: $cache_uid1 u2: $cache_uid2 c12: ", $cache_1_2_ref->{ $cache_uid1 } || q{}, "\n") ; +# or ( ! exists( $cache_1_2_ref->{ $cache_uid1 } ) ) +# or ( ! ( $cache_uid2 == $cache_1_2_ref->{ $cache_uid1 } ) ) + if ( ( not defined $cache_uid1 ) + or ( not defined $cache_uid2 ) + or ( not exists $h1_msgs_all_hash_ref->{ $cache_uid1 } ) + or ( not exists $h2_msgs_all_hash_ref->{ $cache_uid2 } ) + ) { + $debugcache and myprint( "remove $file\n" ) ; + unlink $file or myprint( "$OS_ERROR" ) ; + } + } + + $debugcache and myprint( "Exiting clean_cache\n" ) ; + return( 1 ) ; +} + +sub tests_clean_cache +{ + note( 'Entering tests_clean_cache()' ) ; + + ok( ( not -d 'W/tmp/cache/G1/G2' or rmtree( 'W/tmp/cache/G1/G2' )), 'clean_cache: rmtree W/tmp/cache/G1/G2' ) ; + ok( mkpath( 'W/tmp/cache/G1/G2' ), 'clean_cache: mkpath W/tmp/cache/G1/G2' ) ; + + my @test_files_cache = ( qw( + W/tmp/cache/G1/G2/100_200 + W/tmp/cache/G1/G2/101_201 + W/tmp/cache/G1/G2/120_220 + W/tmp/cache/G1/G2/142_242 + W/tmp/cache/G1/G2/143_243 + W/tmp/cache/G1/G2/177_277 + W/tmp/cache/G1/G2/177_377 + W/tmp/cache/G1/G2/177_777 + W/tmp/cache/G1/G2/155_255 + ) ) ; + ok( touch(@test_files_cache), 'clean_cache: touch W/tmp/cache/G1/G2/...' ) ; + + ok( -f 'W/tmp/cache/G1/G2/100_200', 'clean_cache: 100_200 before' ); + ok( -f 'W/tmp/cache/G1/G2/142_242', 'clean_cache: 142_242 before' ); + ok( -f 'W/tmp/cache/G1/G2/177_277', 'clean_cache: 177_277 before' ); + ok( -f 'W/tmp/cache/G1/G2/177_377', 'clean_cache: 177_377 before' ); + ok( -f 'W/tmp/cache/G1/G2/177_777', 'clean_cache: 177_777 before' ); + ok( -f 'W/tmp/cache/G1/G2/155_255', 'clean_cache: 155_255 before' ); + + my $cache = { + 142 => 242, + 177 => 777, + } ; + + my $all_1 = { + 142 => q{}, + 177 => q{}, + } ; + + my $all_2 = { + 200 => q{}, + 242 => q{}, + 777 => q{}, + } ; + ok( clean_cache( \@test_files_cache, $cache, $all_1, $all_2 ), 'clean_cache: ' ) ; + + ok( ! -f 'W/tmp/cache/G1/G2/100_200', 'clean_cache: 100_200 after' ); + ok( -f 'W/tmp/cache/G1/G2/142_242', 'clean_cache: 142_242 after' ); + ok( ! -f 'W/tmp/cache/G1/G2/177_277', 'clean_cache: 177_277 after' ); + ok( ! -f 'W/tmp/cache/G1/G2/177_377', 'clean_cache: 177_377 after' ); + ok( -f 'W/tmp/cache/G1/G2/177_777', 'clean_cache: 177_777 after' ); + ok( ! -f 'W/tmp/cache/G1/G2/155_255', 'clean_cache: 155_255 after' ); + + note( 'Leaving tests_clean_cache()' ) ; + return ; +} + +sub tests_clean_cache_2 +{ + note( 'Entering tests_clean_cache_2()' ) ; + + ok( ( not -d 'W/tmp/cache/G1/G2' or rmtree( 'W/tmp/cache/G1/G2' )), 'clean_cache_2: rmtree W/tmp/cache/G1/G2' ) ; + ok( mkpath( 'W/tmp/cache/G1/G2' ), 'clean_cache_2: mkpath W/tmp/cache/G1/G2' ) ; + + my @test_files_cache = ( qw( + W/tmp/cache/G1/G2/100_200 + W/tmp/cache/G1/G2/101_201 + W/tmp/cache/G1/G2/120_220 + W/tmp/cache/G1/G2/142_242 + W/tmp/cache/G1/G2/143_243 + W/tmp/cache/G1/G2/177_277 + W/tmp/cache/G1/G2/177_377 + W/tmp/cache/G1/G2/177_777 + W/tmp/cache/G1/G2/155_255 + ) ) ; + ok( touch(@test_files_cache), 'clean_cache_2: touch W/tmp/cache/G1/G2/...' ) ; + + ok( -f 'W/tmp/cache/G1/G2/100_200', 'clean_cache_2: 100_200 before' ); + ok( -f 'W/tmp/cache/G1/G2/142_242', 'clean_cache_2: 142_242 before' ); + ok( -f 'W/tmp/cache/G1/G2/177_277', 'clean_cache_2: 177_277 before' ); + ok( -f 'W/tmp/cache/G1/G2/177_377', 'clean_cache_2: 177_377 before' ); + ok( -f 'W/tmp/cache/G1/G2/177_777', 'clean_cache_2: 177_777 before' ); + ok( -f 'W/tmp/cache/G1/G2/155_255', 'clean_cache_2: 155_255 before' ); + + my $cache = { + 142 => 242, + 177 => 777, + } ; + + my $all_1 = { + $NUMBER_100 => q{}, + 142 => q{}, + 177 => q{}, + } ; + + my $all_2 = { + 200 => q{}, + 242 => q{}, + 777 => q{}, + } ; + + + + ok( clean_cache( \@test_files_cache, $cache, $all_1, $all_2 ), 'clean_cache_2: ' ) ; + + ok( -f 'W/tmp/cache/G1/G2/100_200', 'clean_cache_2: 100_200 after' ); + ok( -f 'W/tmp/cache/G1/G2/142_242', 'clean_cache_2: 142_242 after' ); + ok( ! -f 'W/tmp/cache/G1/G2/177_277', 'clean_cache_2: 177_277 after' ); + ok( ! -f 'W/tmp/cache/G1/G2/177_377', 'clean_cache_2: 177_377 after' ); + ok( -f 'W/tmp/cache/G1/G2/177_777', 'clean_cache_2: 177_777 after' ); + ok( ! -f 'W/tmp/cache/G1/G2/155_255', 'clean_cache_2: 155_255 after' ); + + note( 'Leaving tests_clean_cache_2()' ) ; + return ; +} + + + +sub tests_mkpath +{ + note( 'Entering tests_mkpath()' ) ; + + ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' )), 'mkpath: mkpath W/tmp/tests/' ) ; + + SKIP: { + skip( 'Tests only for Unix', 10 ) if ( 'MSWin32' eq $OSNAME ) ; + my $long_path_unix = '123456789/' x 30 ; + ok( ( -d "W/tmp/tests/long/$long_path_unix" or mkpath( "W/tmp/tests/long/$long_path_unix" ) ), 'mkpath: mkpath 300 char' ) ; + ok( -d "W/tmp/tests/long/$long_path_unix", 'mkpath: mkpath > 300 char verified' ) ; + ok( ( -d "W/tmp/tests/long/$long_path_unix" and rmtree( 'W/tmp/tests/long/' ) ), 'mkpath: rmtree 300 char' ) ; + ok( ! -d "W/tmp/tests/long/$long_path_unix", 'mkpath: rmtree 300 char verified' ) ; + + ok( ( -d 'W/tmp/tests/trailing_dots...' or mkpath( 'W/tmp/tests/trailing_dots...' ) ), 'mkpath: mkpath trailing_dots...' ) ; + ok( -d 'W/tmp/tests/trailing_dots...', 'mkpath: mkpath trailing_dots... verified' ) ; + ok( ( -d 'W/tmp/tests/trailing_dots...' and rmtree( 'W/tmp/tests/trailing_dots...' ) ), 'mkpath: rmtree trailing_dots...' ) ; + ok( ! -d 'W/tmp/tests/trailing_dots...', 'mkpath: rmtree trailing_dots... verified' ) ; + + eval { ok( 1 / 0, 'mkpath: divide by 0' ) ; } or ok( 1, 'mkpath: can not divide by 0' ) ; + ok( 1, 'mkpath: still alive' ) ; + } ; + + SKIP: { + skip( 'Tests only for MSWin32', 13 ) if ( 'MSWin32' ne $OSNAME ) ; + my $long_path_2_prefix = ".\\imapsync_tests" || '\\\?\\E:\\TEMP\\imapsync_tests' ; + myprint( "long_path_2_prefix: $long_path_2_prefix\n" ) ; + + my $long_path_100 = $long_path_2_prefix . '\\' . '123456789\\' x 10 . 'END' ; + my $long_path_300 = $long_path_2_prefix . '\\' . '123456789\\' x 30 . 'END' ; + + #myprint( "$long_path_100\n" ) ; + + ok( ( -d $long_path_2_prefix or mkpath( $long_path_2_prefix ) ), 'mkpath: -d mkpath small path' ) ; + ok( ( -d $long_path_2_prefix ), 'mkpath: -d mkpath small path done' ) ; + ok( ( -d $long_path_100 or mkpath( $long_path_100 ) ), 'mkpath: mkpath > 100 char' ) ; + ok( ( -d $long_path_100 ), 'mkpath: -d mkpath > 200 char done' ) ; + ok( ( -d $long_path_2_prefix and rmtree( $long_path_2_prefix ) ), 'mkpath: rmtree > 100 char' ) ; + ok( (! -d $long_path_2_prefix ), 'mkpath: ! -d rmtree done' ) ; + + # Without the eval the following mkpath 300 just kill the whole process without a whisper + #myprint( "$long_path_300\n" ) ; + eval { ok( ( -d $long_path_300 or mkpath( $long_path_300 ) ), 'mkpath: create a path with 300 characters' ) ; } + or ok( 1, 'mkpath: can not create a path with 300 characters' ) ; + ok( ( ( ! -d $long_path_300 ) or -d $long_path_300 and rmtree( $long_path_300 ) ), 'mkpath: rmtree the 300 character path' ) ; + ok( 1, 'mkpath: still alive' ) ; + + ok( ( -d 'W/tmp/tests/trailing_dots...' or mkpath( 'W/tmp/tests/trailing_dots...' ) ), 'mkpath: mkpath trailing_dots...' ) ; + ok( -d 'W/tmp/tests/trailing_dots...', 'mkpath: mkpath trailing_dots... verified' ) ; + ok( ( -d 'W/tmp/tests/trailing_dots...' and rmtree( 'W/tmp/tests/trailing_dots...' ) ), 'mkpath: rmtree trailing_dots...' ) ; + ok( ! -d 'W/tmp/tests/trailing_dots...', 'mkpath: rmtree trailing_dots... verified' ) ; + + + } ; + + note( 'Leaving tests_mkpath()' ) ; + # Keep this because of the eval used by the caller (failed badly?) + return 1 ; +} + +sub tests_touch +{ + note( 'Entering tests_touch()' ) ; + + ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' )), 'touch: mkpath W/tmp/tests/' ) ; + ok( 1 == touch( 'W/tmp/tests/lala'), 'touch: W/tmp/tests/lala') ; + ok( 1 == touch( 'W/tmp/tests/\y'), 'touch: W/tmp/tests/\y') ; + ok( 0 == touch( '/no/no/no/aaa'), 'touch: not /aaa') ; + ok( 1 == touch( 'W/tmp/tests/lili', 'W/tmp/tests/lolo'), 'touch: 2 files') ; + ok( 0 == touch( 'W/tmp/tests/\y', '/no/no/aaa'), 'touch: 2 files, 1 fails' ) ; + + note( 'Leaving tests_touch()' ) ; + return ; +} + + +sub touch +{ + my @files = @_ ; + my $failures = 0 ; + + foreach my $file ( @files ) { + my $fh = IO::File->new ; + if ( $fh->open(">> $file" ) ) { + $fh->close ; + }else{ + myprint( "Could not open file $file in write/append mode\n" ) ; + $failures++ ; + } + } + return( ! $failures ); +} + + + +sub tests_tmpdir_has_colon_bug +{ + note( 'Entering tests_tmpdir_has_colon_bug()' ) ; + + ok( 0 == tmpdir_has_colon_bug( q{} ), 'tmpdir_has_colon_bug: ' ) ; + ok( 0 == tmpdir_has_colon_bug( '/tmp' ), 'tmpdir_has_colon_bug: /tmp' ) ; + ok( 1 == tmpdir_has_colon_bug( 'C:' ), 'tmpdir_has_colon_bug: C:' ) ; + ok( 1 == tmpdir_has_colon_bug( 'C:\temp' ), 'tmpdir_has_colon_bug: C:\temp' ) ; + + note( 'Leaving tests_tmpdir_has_colon_bug()' ) ; + return ; +} + +sub tmpdir_has_colon_bug +{ + my $path = shift ; + + my $path_filtered = filter_forbidden_characters( $path ) ; + if ( $path_filtered ne $path ) { + ( -d $path_filtered ) and myprint( "Path $path was previously mistakely changed to $path_filtered\n" ) ; + return( 1 ) ; + } + return( 0 ) ; +} + +sub tmpdir_fix_colon_bug +{ + my $mysync = shift ; + my $err = 0 ; + if ( not (-d $mysync->{ tmpdir } and -r _ and -w _) ) { + myprint( "tmpdir $mysync->{ tmpdir } is not valid\n" ) ; + return( 0 ) ; + } + my $cachedir_new = "$mysync->{ tmpdir }/imapsync_cache" ; + + if ( not tmpdir_has_colon_bug( $cachedir_new ) ) { return( 0 ) } ; + + # check if old cache directory already exists + my $cachedir_old = filter_forbidden_characters( $cachedir_new ) ; + if ( not ( -d $cachedir_old ) ) { + myprint( "Old cache directory $cachedir_new no exists, nothing to do\n" ) ; + return( 1 ) ; + } + # check if new cache directory already exists + if ( -d $cachedir_new ) { + myprint( "New fixed cache directory $cachedir_new already exists, not moving the old one $cachedir_old. Fix this manually.\n" ) ; + return( 0 ) ; + }else{ + # move the old one to the new place + myprint( "Moving $cachedir_old to $cachedir_new Do not interrupt this task.\n" ) ; + File::Copy::Recursive::rmove( $cachedir_old, $cachedir_new ) + or do { + myprint( "Could not move $cachedir_old to $cachedir_new\n" ) ; + $err++ ; + } ; + # check it succeeded + if ( -d $cachedir_new and -r _ and -w _ ) { + myprint( "New fixed cache directory $cachedir_new ok\n" ) ; + }else{ + myprint( "New fixed cache directory $cachedir_new does not exist\n" ) ; + $err++ ; + } + if ( -d $cachedir_old ) { + myprint( "Old cache directory $cachedir_old still exists\n" ) ; + $err++ ; + }else{ + myprint( "Old cache directory $cachedir_old successfully moved\n" ) ; + } + } + return( not $err ) ; +} + + +sub tests_cache_folder +{ + note( 'Entering tests_cache_folder()' ) ; + + ok( '/path/fold1/fold2' eq cache_folder( q{}, '/path', 'fold1', 'fold2'), 'cache_folder: /path, fold1, fold2 -> /path/fold1/fold2' ) ; + ok( '/pa_th/fold1/fold2' eq cache_folder( q{}, '/pa*th', 'fold1', 'fold2'), 'cache_folder: /pa*th, fold1, fold2 -> /path/fold1/fold2' ) ; + ok( '/_p_a__th/fol_d1/fold2' eq cache_folder( q{}, '/>pp /path/fol_d1/fold2' ) ; + + ok( 'D:/path/fold1/fold2' eq cache_folder( 'D:', '/path', 'fold1', 'fold2'), 'cache_folder: /path, fold1, fold2 -> /path/fold1/fold2' ) ; + ok( 'D:/pa_th/fold1/fold2' eq cache_folder( 'D:', '/pa*th', 'fold1', 'fold2'), 'cache_folder: /pa*th, fold1, fold2 -> /path/fold1/fold2' ) ; + ok( 'D:/_p_a__th/fol_d1/fold2' eq cache_folder( 'D:', '/>pp /path/fol_d1/fold2' ) ; + ok( '//' eq cache_folder( q{}, q{}, q{}, q{}), 'cache_folder: -> //' ) ; + ok( '//_______' eq cache_folder( q{}, q{}, q{}, '*|?:"<>'), 'cache_folder: *|?:"<> -> //_______' ) ; + + note( 'Leaving tests_cache_folder()' ) ; + return ; +} + +sub cache_folder +{ + my( $cache_base, $cache_dir, $h1_fold, $h2_fold ) = @_ ; + + my $sep_1 = $sync->{ h1_sep } || '/'; + my $sep_2 = $sync->{ h2_sep } || '/'; + + #myprint( "$cache_dir h1_fold $h1_fold sep1 $sep_1 h2_fold $h2_fold sep2 $sep_2\n" ) ; + $h1_fold = convert_sep_to_slash( $h1_fold, $sep_1 ) ; + $h2_fold = convert_sep_to_slash( $h2_fold, $sep_2 ) ; + + my $cache_folder = "$cache_base" . filter_forbidden_characters( "$cache_dir/$h1_fold/$h2_fold" ) ; + #myprint( "cache_folder [$cache_folder]\n" ) ; + return( $cache_folder ) ; +} + +sub tests_filter_forbidden_characters +{ + note( 'Entering tests_filter_forbidden_characters()' ) ; + + is( undef , filter_forbidden_characters( ), 'filter_forbidden_characters: no args -> undef' ) ; + + is( 'a_b' , filter_forbidden_characters( 'a_b' ), 'filter_forbidden_characters: a_b -> a_b' ) ; + is( 'a_b' , filter_forbidden_characters( 'a*b' ), 'filter_forbidden_characters: a*b -> a_b' ) ; + is( 'a_b' , filter_forbidden_characters( 'a|b' ), 'filter_forbidden_characters: a|b -> a_b' ) ; + is( 'a_b' , filter_forbidden_characters( 'a?b' ), 'filter_forbidden_characters: a?b -> a_b' ) ; + is( 'a________b', filter_forbidden_characters( q{a*|?:"<>'b} ), q{filter_forbidden_characters: a*|?:"<>'b -> a________b} ) ; + + + is( 'a_b_' , filter_forbidden_characters( 'a b ' ), 'filter_forbidden_characters: "a b " -> "a_b_"' ) ; + + + is( 'a_b' , filter_forbidden_characters( "a\tb" ), 'filter_forbidden_characters: a\tb -> a_b' ) ; + is( "a_b" , filter_forbidden_characters( "a\rb" ), 'filter_forbidden_characters: a\rb -> a_b' ) ; + is( "a_b" , filter_forbidden_characters( "a\nb" ), 'filter_forbidden_characters: a\nb -> a_b' ) ; + is( "a_b" , filter_forbidden_characters( "a\\b" ), 'filter_forbidden_characters: a\b -> a_b' ) ; + + is( 'a-b' , filter_forbidden_characters( 'a-b' ), 'filter_forbidden_characters: a-b -> a-b' ) ; + is( 'a__-__-__-__-__b' , filter_forbidden_characters( 'aé-è-à -ç-Öb' ), 'filter_forbidden_characters: aé-è-à -ç-Öb -> a__-__-__-__-__b' ) ; + + is( 'abcdABCDwxyzWXYZ012789' , filter_forbidden_characters( 'abcdABCDwxyzWXYZ012789' ), + 'filter_forbidden_characters: abcdABCDwxyzWXYZ012789 -> abcdABCDwxyzWXYZ012789' ) ; + + + note( 'Leaving tests_filter_forbidden_characters()' ) ; + return ; +} + +sub filter_forbidden_characters +{ + my $string = shift ; + + if ( ! defined $string ) { return ; } + + $string =~ s{[\Q*|?:"<>' \E\t\r\n\\]}{_}xg ; + # replace all non-ascii and control characters by _ + $string =~ s/[[:^ascii:][:cntrl:]]/_/xg ; + + #myprint( "[$string]\n" ) ; + return( $string ) ; +} + +sub tests_convert_sep_to_slash +{ + note( 'Entering tests_convert_sep_to_slash()' ) ; + + + ok(q{} eq convert_sep_to_slash(q{}, '/'), 'convert_sep_to_slash: no folder'); + ok('INBOX' eq convert_sep_to_slash('INBOX', '/'), 'convert_sep_to_slash: INBOX'); + ok('INBOX/foo' eq convert_sep_to_slash('INBOX/foo', '/'), 'convert_sep_to_slash: INBOX/foo'); + ok('INBOX/foo' eq convert_sep_to_slash('INBOX_foo', '_'), 'convert_sep_to_slash: INBOX_foo'); + ok('INBOX/foo/zob' eq convert_sep_to_slash('INBOX_foo_zob', '_'), 'convert_sep_to_slash: INBOX_foo_zob'); + ok('INBOX/foo' eq convert_sep_to_slash('INBOX.foo', '.'), 'convert_sep_to_slash: INBOX.foo'); + ok('INBOX/foo/hi' eq convert_sep_to_slash('INBOX.foo.hi', '.'), 'convert_sep_to_slash: INBOX.foo.hi'); + + note( 'Leaving tests_convert_sep_to_slash()' ) ; + return ; +} + +sub convert_sep_to_slash +{ + my ( $folder, $sep ) = @_ ; + + $folder =~ s{\Q$sep\E}{/}xg ; + return( $folder ) ; +} + + +sub tests_regexmess +{ + note( 'Entering tests_regexmess()' ) ; + + ok( 'blabla' eq regexmess( 'blabla' ), 'regexmess: no regexmess, nothing to do' ) ; + + @regexmess = ( 'lalala' ) ; + ok( not( defined regexmess( 'popopo' ) ), 'regexmess: bad regex lalala' ) ; + + @regexmess = ( 's/p/Z/g' ) ; + ok( 'ZoZoZo' eq regexmess( 'popopo' ), 'regexmess: s/p/Z/g' ) ; + + @regexmess = ( 's{c}{C}gxms' ) ; + ok("H1: abC\nH2: Cde\n\nBody abC" + eq regexmess( "H1: abc\nH2: cde\n\nBody abc"), + 'regexmess: c->C'); + + @regexmess = ( 's{\AFrom\ }{From:}gxms' ) ; + ok( q{} + eq regexmess(q{}), + 'regexmess: From mbox 1 add colon blank'); + + ok( 'From:' + eq regexmess('From '), + 'regexmess: From mbox 2 add colo'); + + ok( "\n" . 'From ' + eq regexmess("\n" . 'From '), + 'regexmess: From mbox 3 add colo') ; + + ok( "From: zzz\n" . 'From ' + eq regexmess("From zzz\n" . 'From '), + 'regexmess: From mbox 4 add colo') ; + + @regexmess = ( 's{\AFrom\ [^\n]*(\n)?}{}gxms' ) ; + ok( q{} + eq regexmess(q{}), + 'regexmess: From mbox 1 remove, blank'); + + ok( q{} + eq regexmess('From '), + 'regexmess: From mbox 2 remove'); + + ok( "\n" . 'From ' + eq regexmess("\n" . 'From '), + 'regexmess: From mbox 3 remove'); + + #myprint( "[", regexmess("From zzz\n" . 'From '), "]" ) ; + ok( q{} . 'From ' + eq regexmess("From zzz\n" . 'From '), + 'regexmess: From mbox 4 remove'); + + + is( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, +Bye. +EOM + , regexmess( +<<'EOM' +From zzz +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, +Bye. +EOM + ), 'regexmess: From mbox 5 remove'); + + +@regexmess = ( 's{\A((?:[^\n]+\n)+|)^Disposition-Notification-To:[^\n]*\n(\r?\n|.*\n\r?\n)}{$1$2}xms' ) ; # SUPER SUPER BEST! + ok( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, +Bye. +EOM + eq regexmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +Disposition-Notification-To: Gilles LAMIRAL +From: + +Hello, +Bye. +EOM + ), + 'regexmess: 1 Delete header Disposition-Notification-To:'); + + ok( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, +Bye. +EOM + eq regexmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: +Disposition-Notification-To: Gilles LAMIRAL + +Hello, +Bye. +EOM + ), + 'regexmess: 2 Delete header Disposition-Notification-To:'); + + ok( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, +Bye. +EOM + eq regexmess( +<<'EOM' +Disposition-Notification-To: Gilles LAMIRAL +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, +Bye. +EOM + ), + 'regexmess: 3 Delete header Disposition-Notification-To:'); + + ok( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Disposition-Notification-To: Gilles LAMIRAL +Bye. +EOM + eq regexmess( +<<'EOM' +Disposition-Notification-To: Gilles LAMIRAL +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Disposition-Notification-To: Gilles LAMIRAL +Bye. +EOM + ), + 'regexmess: 4 Delete header Disposition-Notification-To:'); + + + ok( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Disposition-Notification-To: Gilles LAMIRAL +Bye. +EOM + eq regexmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Disposition-Notification-To: Gilles LAMIRAL +Bye. +EOM + ), + 'regexmess: 5 Delete header Disposition-Notification-To:'); + + + ok( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, +Disposition-Notification-To: Gilles LAMIRAL +Bye. +EOM + eq regexmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, +Disposition-Notification-To: Gilles LAMIRAL +Bye. +EOM + ), + 'regexmess: 6 Delete header Disposition-Notification-To:'); + + ok( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, +Disposition-Notification-To: Gilles LAMIRAL + +Bye. +EOM + eq regexmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, +Disposition-Notification-To: Gilles LAMIRAL + +Bye. +EOM + ), + 'regexmess: 7 Delete header Disposition-Notification-To:'); + + + ok( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, +Bye. +EOM + eq regexmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, +Bye. +EOM +), + 'regexmess: 8 Delete header Disposition-Notification-To:'); + + + ok( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, +Disposition-Notification-To: Gilles LAMIRAL +Bye. +EOM + eq regexmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, +Disposition-Notification-To: Gilles LAMIRAL +Bye. +EOM + ), + 'regexmess: 9 Delete header Disposition-Notification-To:'); + + + + ok( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, +Disposition-Notification-To: Gilles LAMIRAL + + +Bye. +EOM + eq regexmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, +Disposition-Notification-To: Gilles LAMIRAL + + +Bye. +EOM + ), + 'regexmess: 10 Delete header Disposition-Notification-To:'); + + ok( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, + +Disposition-Notification-To: Gilles LAMIRAL + +Bye. +EOM + eq regexmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, + +Disposition-Notification-To: Gilles LAMIRAL + +Bye. +EOM +), + 'regexmess: 11 Delete header Disposition-Notification-To:'); + + ok( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, + +Disposition-Notification-To: Gilles LAMIRAL + +Disposition-Notification-To: Gilles LAMIRAL + +Bye. +EOM + eq regexmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, + +Disposition-Notification-To: Gilles LAMIRAL + +Disposition-Notification-To: Gilles LAMIRAL + +Bye. +EOM + ), + 'regexmess: 12 Delete header Disposition-Notification-To:'); + + + @regexmess = ( 's{\A(.*?(?! ^$))^Disposition-Notification-To:(.*?)$}{$1X-Disposition-Notification-To:$2}igxms' ) ; # BAD! + @regexmess = ( 's{\A((?:[^\n]+\n)+|)(^Disposition-Notification-To:[^\n]*\n)(\r?\n|.*\n\r?\n)}{$1X-$2$3}ims' ) ; + + + ok( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, + +Disposition-Notification-To: Gilles LAMIRAL + +Disposition-Notification-To: Gilles LAMIRAL + +Bye. +EOM + eq regexmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, + +Disposition-Notification-To: Gilles LAMIRAL + +Disposition-Notification-To: Gilles LAMIRAL + +Bye. +EOM + ), + 'regexmess: 13 Delete header Disposition-Notification-To:'); + + ok( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +X-Disposition-Notification-To: Gilles LAMIRAL +From: + +Hello, + +Disposition-Notification-To: Gilles LAMIRAL + +Disposition-Notification-To: Gilles LAMIRAL + +Bye. +EOM + eq regexmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +Disposition-Notification-To: Gilles LAMIRAL +From: + +Hello, + +Disposition-Notification-To: Gilles LAMIRAL + +Disposition-Notification-To: Gilles LAMIRAL + +Bye. +EOM + ), + 'regexmess: 14 Delete header Disposition-Notification-To:'); + + ok( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +X-Disposition-Notification-To: Gilles LAMIRAL +From: + +Hello, + +Bye. +EOM + eq regexmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +Disposition-Notification-To: Gilles LAMIRAL +From: + +Hello, + +Bye. +EOM + ), + 'regexmess: 15 Delete header Disposition-Notification-To:'); + + + ok( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: +X-Disposition-Notification-To: Gilles LAMIRAL + +Hello, + +Bye. +EOM + eq regexmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: +Disposition-Notification-To: Gilles LAMIRAL + +Hello, + +Bye. +EOM + ), + 'regexmess: 16 Delete header Disposition-Notification-To:'); + + ok( +<<'EOM' +X-Disposition-Notification-To: Gilles LAMIRAL +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, + +Bye. +EOM + eq regexmess( +<<'EOM' +Disposition-Notification-To: Gilles LAMIRAL +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, + +Bye. +EOM +), + 'regexmess: 17 Delete header Disposition-Notification-To:'); + + @regexmess = ( 's/.{11}\K.*//gs' ) ; + is( "0123456789\n", regexmess( "0123456789\n" x 100 ), 'regexmess: truncate whole message after 11 characters' ) ; + is( "0123456789\n", regexmess( "0123456789\n" x 100_000 ), 'regexmess: truncate whole message after 11 characters ~ 1MB' ) ; + + @regexmess = ( 's/.{10000}\K.*//gs' ) ; + is( "123456789\n" x 1000, regexmess( "123456789\n" x 100_000 ), 'regexmess: truncate whole message after 10000 characters ~ 1MB' ) ; + + @regexmess = ( 's/^(X-Ham-Report.*?\n)^X-/X-/sm' ) ; + + is( +<<'EOM' +X-Spam-Score: -1 +X-Spam-Bar: / +X-Spam-Flag: NO +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, + +Bye. +EOM +, + regexmess( +<<'EOM' +X-Spam-Score: -1 +X-Spam-Bar: / +X-Ham-Report: =?utf-8?Q?Spam_detection_software=2C_running?= + =?utf-8?Q?_on_the_system_=22ohp-ag006.int200?= +_has_NOT_identified_thi?= + =?utf-8?Q?s_incoming_email_as_spam.__The_o?= +_message_has_been_attac?= + =?utf-8?Q?hed_to_this_so_you_can_view_it_o?= +___________________________?= + =?utf-8?Q?__author's_domain +X-Spam-Flag: NO +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, + +Bye. +EOM + ), + 'regexmess: Delete header X-Ham-Report:'); + + +# regex to play with Date: from the FAQ +#@regexmess = 's{\A(.*?(?! ^$))^Date:(.*?)$}{$1Date:$2\nX-Date:$2}gxms' + + +# Change 8bit characters in whole email to X characters + @regexmess = ( 's{[\x80-\xff]}{X}gxms' ) ; + is( 'X-8bit: kaka 1 XX kiki', regexmess('X-8bit: kaka 1 ¤ kiki'), 'regexmess: 1 Change 8bit characters in whole email to X characters'); + +# Same change but using tr + @regexmess = ( 'tr [\x80-\xff] [X]' ) ; + is( 'X-8bit: kaka 1 XXXX kiki', regexmess('X-8bit: kaka 1 ¤£ kiki'), 'regexmess: 2 Change 8bit characters in whole email to X characters, using tr'); + + + +# Add a final \r\n if missing + @regexmess = ( 's{(? +LaSuite: super + +Hello, +Bye. +EOM + , regexmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: +X-Spam-Report: caca +caca + caca +caca +LaSuite: super + +Hello, +Bye. +EOM + ), 'regexmess: 1 remove buggy X-Spam-Report: across several lines, not the final header'); + + + is( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: +LaSuite: super +LaSuite2: super 2 + +Hello, +Bye. +EOM + , regexmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: +X-Spam-Report: caca +caca + caca +caca +LaSuite: super +LaSuite2: super 2 + +Hello, +Bye. +EOM + ), 'regexmess: 2 remove buggy X-Spam-Report: across several lines, not the final header'); + + + is( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: +LaSuite: super +LaSuite2: super 2 + +Hello, +Bye. +EOM + , regexmess( +<<'EOM' +X-Spam-Report: caca +caca + caca +caca +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: +LaSuite: super +LaSuite2: super 2 + +Hello, +Bye. +EOM + ), 'regexmess: 3 remove buggy X-Spam-Report: across several lines, first header'); + + + + + is( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, +Bye. +EOM + , regexmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: +X-Spam-Report: caca +caca + caca +caca + +Hello, +Bye. +EOM + ), 'regexmess: 4 remove buggy X-Spam-Report: across several lines, final header'); + + + is( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, +Bye. +EOM + , regexmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello, +Bye. +EOM + ), 'regexmess: 5 remove buggy X-Spam-Report: not there at all'); + + + is( +<<"EOM" +Date: Sat, 10 Jul 2010 05:34:45 -0700\r +From:\r +LaSuite: super\r +LaSuite2: super 2\r +\r +Hello,\r +Bye.\r +EOM + , regexmess( +<<"EOM" +X-Spam-Report: caca\r +caca\r + caca\r +caca\r +Date: Sat, 10 Jul 2010 05:34:45 -0700\r +From:\r +LaSuite: super\r +LaSuite2: super 2\r +\r +Hello,\r +Bye.\r +EOM + ), 'regexmess: 6 remove buggy X-Spam-Report: across several lines, first header, with \r'); + + + is( +<<"EOM" +Date: Sat, 10 Jul 2010 05:34:45 -0700\r +From:\r +LaSuite: super\r +LaSuite2: super 2\r +\r +Hello,\r +Bye.\r +EOM + , regexmess( +<<"EOM" +Date: Sat, 10 Jul 2010 05:34:45 -0700\r +From:\r +X-Spam-Report: caca\r +caca\r + caca\r +caca\r +LaSuite: super\r +LaSuite2: super 2\r +\r +Hello,\r +Bye.\r +EOM + ), 'regexmess: 7 remove buggy X-Spam-Report: across several lines, middle header, with \r'); + + + is( +<<"EOM" +Date: Sat, 10 Jul 2010 05:34:45 -0700\r +From:\r +\r +Hello,\r +Bye.\r +EOM + , regexmess( +<<"EOM" +Date: Sat, 10 Jul 2010 05:34:45 -0700\r +From:\r +X-Spam-Report: caca\r +caca\r + caca\r +caca\r +\r +Hello,\r +Bye.\r +EOM + ), 'regexmess: 8 remove buggy X-Spam-Report: across several lines, final header, with \r'); + + + undef @regexmess ; + note( 'Leaving tests_regexmess()' ) ; + return ; +} + +sub regexmess +{ + my ( $string ) = @_ ; + foreach my $regexmess ( @regexmess ) { + $sync->{ debug } and myprint( "eval \$string =~ $regexmess\n" ) ; + my $ret = eval "\$string =~ $regexmess ; 1" ; + #myprint( "eval [$ret]\n" ) ; + if ( ( not $ret ) or $EVAL_ERROR ) { + myprint( "Error: eval regexmess '$regexmess': $EVAL_ERROR" ) ; + return( undef ) ; + } + } + $sync->{ debug } and myprint( "$string\n" ) ; + return( $string ) ; +} + + +sub tests_skipmess +{ + note( 'Entering tests_skipmess()' ) ; + + ok( not( defined skipmess( 'blabla' ) ), 'skipmess, no skipmess, no skip' ) ; + + @skipmess = ('[') ; + ok( not( defined skipmess( 'popopo' ) ), 'skipmess, bad regex [' ) ; + + @skipmess = ('lalala') ; + ok( not( defined skipmess( 'popopo' ) ), 'skipmess, bad regex lalala' ) ; + + @skipmess = ('/popopo/') ; + ok( 1 == skipmess( 'popopo' ), 'skipmess, popopo match regex /popopo/' ) ; + + @skipmess = ('/popopo/') ; + ok( 0 == skipmess( 'rrrrrr' ), 'skipmess, rrrrrr does not match regex /popopo/' ) ; + + @skipmess = ('m{^$}') ; + ok( 1 == skipmess( q{} ), 'skipmess: empty string yes' ) ; + ok( 0 == skipmess( 'Hi!' ), 'skipmess: empty string no' ) ; + + @skipmess = ('m{i}') ; + ok( 1 == skipmess( 'Hi!' ), 'skipmess: i string yes' ) ; + ok( 0 == skipmess( 'Bye!' ), 'skipmess: i string no' ) ; + + @skipmess = ('m{[\x80-\xff]}') ; + ok( 0 == skipmess( 'Hi!' ), 'skipmess: i 8bit no' ) ; + ok( 1 == skipmess( "\xff" ), 'skipmess: \xff 8bit yes' ) ; + + @skipmess = ('m{A}', 'm{B}') ; + ok( 0 == skipmess( 'Hi!' ), 'skipmess: A or B no' ) ; + ok( 0 == skipmess( 'lala' ), 'skipmess: A or B no' ) ; + ok( 0 == skipmess( "\xff" ), 'skipmess: A or B no' ) ; + ok( 1 == skipmess( 'AB' ), 'skipmess: A or B yes' ) ; + ok( 1 == skipmess( 'BA' ), 'skipmess: A or B yes' ) ; + ok( 1 == skipmess( 'AA' ), 'skipmess: A or B yes' ) ; + ok( 1 == skipmess( 'Ok Bye' ), 'skipmess: A or B yes' ) ; + + + @skipmess = ( 'm#\A((?:[^\n]+\n)+|)^Content-Type: Message/Partial;[^\n]*\n(?:\n|.*\n\n)#ism' ) ; # SUPER BEST! + + + + ok( 1 == skipmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +Content-Type: Message/Partial; blabla +From: + +Hello! +Bye. +EOM +), + 'skipmess: 1 match Content-Type: Message/Partial' ) ; + + ok( 0 == skipmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello! +Bye. +EOM +), + 'skipmess: 2 not match Content-Type: Message/Partial' ) ; + + + ok( 1 == skipmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: +Content-Type: Message/Partial; blabla + +Hello! +Bye. +EOM +), + 'skipmess: 3 match Content-Type: Message/Partial' ) ; + + ok( 0 == skipmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello! +Content-Type: Message/Partial; blabla +Bye. +EOM +), + 'skipmess: 4 not match Content-Type: Message/Partial' ) ; + + + ok( 0 == skipmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Hello! +Content-Type: Message/Partial; blabla + +Bye. +EOM +), + 'skipmess: 5 not match Content-Type: Message/Partial' ) ; + + + ok( 1 == skipmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +Content-Type: Message/Partial; blabla +From: + +Hello! + +Content-Type: Message/Partial; blabla + +Bye. +EOM +), + 'skipmess: 6 match Content-Type: Message/Partial' ) ; + + ok( 1 == skipmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +Content-Type: Message/Partial; +From: + +Hello! +Bye. +EOM +), + 'skipmess: 7 match Content-Type: Message/Partial' ) ; + + ok( 1 == skipmess( +<<'EOM' +Date: Wed, 2 Jul 2014 02:26:40 +0000 +MIME-Version: 1.0 +Content-Type: message/partial; + id="TAN_U_P<1404267997.00007489ed17>"; + number=3; + total=3 + +6HQ6Hh3CdXj77qEGixerQ6zHx0OnQ/Cf5On4W0Y6vtU2crABZQtD46Hx1EOh8dDz4+OnTr1G + + +Hello! +Bye. +EOM +), + 'skipmess: 8 match Content-Type: Message/Partial' ) ; + + +ok( 1 == skipmess( +<<'EOM' +Return-Path: +Received: by lamiral.info (Postfix, from userid 1000) + id 21EB12443BF; Mon, 2 Mar 2015 15:38:35 +0100 (CET) +Subject: test: aethaecohngiexao +To: +X-Mailer: mail (GNU Mailutils 2.2) +Message-Id: <20150302143835.21EB12443BF@lamiral.info> +Content-Type: message/partial; + id="TAN_U_P<1404267997.00007489ed17>"; + number=3; + total=3 +Date: Mon, 2 Mar 2015 15:38:34 +0100 (CET) +From: gilles@lamiral.info (Gilles LAMIRAL) + +test: aethaecohngiexao +EOM +), + 'skipmess: 9 match Content-Type: Message/Partial' ) ; + +ok( 1 == skipmess( +<<'EOM' +Date: Mon, 2 Mar 2015 15:38:34 +0100 (CET) +From: gilles@lamiral.info (Gilles LAMIRAL) +Content-Type: message/partial; + id="TAN_U_P<1404267997.00007489ed17>"; + number=3; + total=3 + +test: aethaecohngiexao +EOM +. "lalala\n" x 3_000_000 +), + 'skipmess: 10 match Content-Type: Message/Partial' ) ; + +ok( 0 == skipmess( +<<'EOM' +Date: Mon, 2 Mar 2015 15:38:34 +0100 (CET) +From: gilles@lamiral.info (Gilles LAMIRAL) + +test: aethaecohngiexao +EOM +. "lalala\n" x 3_000_000 +), + 'skipmess: 11 match Content-Type: Message/Partial' ) ; + + +ok( 0 == skipmess( +<<"EOM" +From: fff\r +To: fff\r +Subject: Testing imapsync --skipmess\r +Date: Mon, 22 Aug 2011 08:40:20 +0800\r +Mime-Version: 1.0\r +Content-Type: text/plain; charset=iso-8859-1\r +Content-Transfer-Encoding: 7bit\r +\r +EOM +. qq{!#"d%&'()*+,-./0123456789:;<=>?\@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefg\r\n } x 32_730 +), + 'skipmess: 12 not match Content-Type: Message/Partial' ) ; + # Complex regular subexpression recursion limit (32766) exceeded with more lines + # exit; + + + undef @skipmess ; + note( 'Leaving tests_skipmess()' ) ; + return ; +} + + +sub tests_skipmess_neg +{ + note( 'Entering tests_skipmess_neg()' ) ; + + + @skipmess = ('m{i}') ; + ok( 1 == skipmess( 'Hi!' ), 'skipmess: i string yes' ) ; + ok( 0 == skipmess( 'Ho!' ), 'skipmess: i string no' ) ; + + @skipmess = ('m{\A(?!.*i)}') ; + ok( 0 == skipmess( 'Hi!' ), 'skipmess: not i string no' ) ; + ok( 1 == skipmess( 'Ho!' ), 'skipmess: not i string yes' ) ; + + + @skipmess = ('m{\A(?!.*^From:[^\n]*tartanpion\@machin\.truc)}xms') ; + + ok( 0 == skipmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Bye. +EOM +), + 'skipmess: 1 not From tartanpion@machin.truc' ) ; + +ok( 1 == skipmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + +Bye. +EOM +), + 'skipmess: 2 not From tartanpion@machin.truc' ) ; + + + + + ok( 0 == skipmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + + From: +Bye. +EOM +), + 'skipmess: 3 not From tartanpion@machin.truc' ) ; + +ok( 1 == skipmess( +<<'EOM' +Date: Sat, 10 Jul 2010 05:34:45 -0700 +From: + + From: +Bye. +EOM +), + 'skipmess: 4 not From tartanpion@machin.truc' ) ; + + + + + undef @skipmess ; + note( 'Leaving tests_skipmess_neg()' ) ; + return ; +} + + +sub skipmess +{ + my ( $string ) = @_ ; + my $match ; + #myprint( "$string\n" ) ; + foreach my $skipmess ( @skipmess ) { + $sync->{ debug } and myprint( "eval \$match = \$string =~ $skipmess\n" ) ; + my $ret = eval "\$match = \$string =~ $skipmess ; 1" ; + #myprint( "eval [$ret]\n" ) ; + $sync->{ debug } and myprint( "match [$match]\n" ) ; + if ( ( not $ret ) or $EVAL_ERROR ) { + myprint( "Error: eval skipmess '$skipmess': $EVAL_ERROR" ) ; + return( undef ) ; + } + return( $match ) if ( $match ) ; + } + return( $match ) ; +} + + + + +sub tests_bytes_display_string_bin +{ + note( 'Entering tests_bytes_display_string_bin()' ) ; + + is( 'NA', bytes_display_string_bin( ), 'bytes_display_string_bin: no args => NA' ) ; + is( 'NA', bytes_display_string_bin( undef ), 'bytes_display_string_bin: undef => NA' ) ; + is( 'NA', bytes_display_string_bin( 'blabla' ), 'bytes_display_string_bin: blabla => NA' ) ; + + is( '0.000 KiB', bytes_display_string_bin( 0 ), 'bytes_display_string_bin: 0 => 0.000 KiB' ) ; + is( '0.001 KiB', bytes_display_string_bin( 1 ), 'bytes_display_string_bin: 1 => 0.001 KiB' ) ; + is( '0.010 KiB', bytes_display_string_bin( 10 ), 'bytes_display_string_bin: 10 => 0.010 KiB' ) ; + is( '0.976 KiB', bytes_display_string_bin( 999 ), 'bytes_display_string_bin: 999 => 0.976 KiB' ) ; + note( bytes_display_string_bin( 999 ) ) ; + + is( '0.999 KiB', bytes_display_string_bin( 1023 ), 'bytes_display_string_bin: 1023 => 0.999 KiB' ) ; + note( bytes_display_string_bin( 1023 ) ) ; + is( '1.000 KiB', bytes_display_string_bin( 1024 ), 'bytes_display_string_bin: 1024 => 1.000 KiB' ) ; + note( bytes_display_string_bin( 1024 ) ) ; + is( '1.001 KiB', bytes_display_string_bin( 1025 ), 'bytes_display_string_bin: 1025 => 1.001 KiB' ) ; + + is( '9.999 KiB', bytes_display_string_bin( 10_239 ), 'bytes_display_string_bin: 10_239 => 9.999 KiB' ) ; + note( bytes_display_string_bin( 10_239 ) ) ; + + is( '10.000 KiB', bytes_display_string_bin( 10_240 ), 'bytes_display_string_bin: 10_240 => 10.000 KiB' ) ; + note( bytes_display_string_bin( 10_240 ) ) ; + + is( '999.999 KiB', bytes_display_string_bin( 1_023_999 ), 'bytes_display_string_bin: 1_023_999 => 999.999 KiB' ) ; + note( bytes_display_string_bin( 1_023_999 ) ) ; + + is( '0.977 MiB', bytes_display_string_bin( 1_024_000 ), 'bytes_display_string_bin: 1_024_000 => 0.977 MiB' ) ; + note( bytes_display_string_bin( 1_024_000 ) ) ; + + is( '0.999 MiB', bytes_display_string_bin( 1_047_527 ), 'bytes_display_string_bin: 1_047_527 => 0.999 MiB' ) ; + note( bytes_display_string_bin( 1_047_527 ) ) ; + + is( '0.999 MiB', bytes_display_string_bin( 1_048_051 ), 'bytes_display_string_bin: 1_048_051 => 0.999 MiB' ) ; + note( bytes_display_string_bin( 1_048_051 ) ) ; + + is( '1.000 MiB', bytes_display_string_bin( 1_048_052 ), 'bytes_display_string_bin: 1_048_052 => 1.000 MiB' ) ; + note( bytes_display_string_bin( 1_048_052 ) ) ; + + is( '1.000 MiB', bytes_display_string_bin( 1_048_575 ), 'bytes_display_string_bin: 1_048_575 => 1.000 MiB' ) ; + is( '1.000 MiB', bytes_display_string_bin( 1_048_576 ), 'bytes_display_string_bin: 1_048_576 => 1.000 MiB' ) ; + + is( '1.000 GiB', bytes_display_string_bin( 1_073_741_823 ), 'bytes_display_string_bin: 1_073_741_823 => 1.000 GiB' ) ; + is( '1.000 GiB', bytes_display_string_bin( 1_073_741_824 ), 'bytes_display_string_bin: 1_073_741_824 => 1.000 GiB' ) ; + + + is( '1.000 TiB', bytes_display_string_bin( 1_099_511_627_775 ), 'bytes_display_string_bin: 1_099_511_627_775 => 1.000 TiB' ) ; + is( '1.000 TiB', bytes_display_string_bin( 1_099_511_627_776 ), 'bytes_display_string_bin: 1_099_511_627_776 => 1.000 TiB' ) ; + + is( '1.000 PiB', bytes_display_string_bin( 1_125_899_906_842_623 ), 'bytes_display_string_bin: 1_125_899_906_842_623 => 1.000 PiB' ) ; + is( '1.000 PiB', bytes_display_string_bin( 1_125_899_906_842_624 ), 'bytes_display_string_bin: 1_125_899_906_842_624 => 1.000 PiB' ) ; + + is( '1024.000 PiB', bytes_display_string_bin( 1_152_921_504_606_846_975 ), 'bytes_display_string_bin: 1_152_921_504_606_846_975 => 1024.000 PiB' ) ; + is( '1024.000 PiB', bytes_display_string_bin( 1_152_921_504_606_846_976 ), 'bytes_display_string_bin: 1_152_921_504_606_846_976 => 1024.000 PiB' ) ; + + is( '1048576.000 PiB', bytes_display_string_bin( 1_180_591_620_717_411_303_424 ), 'bytes_display_string_bin: 1_180_591_620_717_411_303_424 => 1048576.000 PiB' ) ; + note( bytes_display_string_bin( 1_180_591_620_717_411_303_424 ) ) ; + note( bytes_display_string_bin( 3_000_000_000 ) ) ; + note( 'Leaving tests_bytes_display_string_bin()' ) ; + + return ; +} + +sub bytes_display_string_bin +{ + my ( $bytes ) = @_ ; + + my $readable_value = q{} ; + + if ( ! defined( $bytes ) ) { + return( 'NA' ) ; + } + + if ( not match_number( $bytes ) ) { + return( 'NA' ) ; + } + + + + SWITCH: { + if ( abs( $bytes ) < ( 1000 * $KIBI ) ) { + $readable_value = mysprintf( '%.3f KiB', $bytes / $KIBI) ; + last SWITCH ; + } + if ( abs( $bytes ) < ( 1000 * $KIBI * $KIBI ) ) { + $readable_value = mysprintf( '%.3f MiB', $bytes / ($KIBI * $KIBI) ) ; + last SWITCH ; + } + if ( abs( $bytes ) < ( 1000 * $KIBI * $KIBI * $KIBI) ) { + $readable_value = mysprintf( '%.3f GiB', $bytes / ($KIBI * $KIBI * $KIBI) ) ; + last SWITCH ; + } + if ( abs( $bytes ) < ( 1000 * $KIBI * $KIBI * $KIBI * $KIBI) ) { + $readable_value = mysprintf( '%.3f TiB', $bytes / ($KIBI * $KIBI * $KIBI * $KIBI) ) ; + last SWITCH ; + } else { + $readable_value = mysprintf( '%.3f PiB', $bytes / ($KIBI * $KIBI * $KIBI * $KIBI * $KIBI) ) ; + } + # if you have exabytes (EiB) of email to transfer, you have too much email! + } + #myprint( "$bytes = $readable_value\n" ) ; + return( $readable_value ) ; +} + +sub tests_bytes_display_string_dec +{ + note( 'Entering tests_bytes_display_string_dec()' ) ; + + is( 'NA', bytes_display_string_dec( ), 'bytes_display_string_dec: no args => NA' ) ; + is( 'NA', bytes_display_string_dec( undef ), 'bytes_display_string_dec: undef => NA' ) ; + is( 'NA', bytes_display_string_dec( 'blabla' ), 'bytes_display_string_dec: blabla => NA' ) ; + + is( '0 bytes', bytes_display_string_dec( 0 ), 'bytes_display_string_dec: 0 => 0 bytes' ) ; + is( '1 bytes', bytes_display_string_dec( 1 ), 'bytes_display_string_dec: 1 => 1 bytes' ) ; + is( '10 bytes', bytes_display_string_dec( 10 ), 'bytes_display_string_dec: 10 => 10 bytes' ) ; + is( '999 bytes', bytes_display_string_dec( 999 ), 'bytes_display_string_dec: 999 => 999 bytes' ) ; + + is( '1.000 KB', bytes_display_string_dec( 1000 ), 'bytes_display_string_dec: 1000 => 1.000 KB' ) ; + is( '1.001 KB', bytes_display_string_dec( 1001 ), 'bytes_display_string_dec: 1000 => 1.1001 KB' ) ; + + is( '999.999 KB', bytes_display_string_dec( 999_999 ), 'bytes_display_string_dec: 999_999 => 999.999 KB' ) ; + + is( '1.000 MB', bytes_display_string_dec( 1_000_000 ), 'bytes_display_string_dec: 1_000_000 => 1.000 MB' ) ; + is( '1.000 MB', bytes_display_string_dec( 1_000_500 ), 'bytes_display_string_dec: 1_000_500 => 1.000 MB' ) ; + is( '1.001 MB', bytes_display_string_dec( 1_000_501 ), 'bytes_display_string_dec: 1_000_501 => 1.001 MB' ) ; + is( '999.999 MB', bytes_display_string_dec( 999_999_000 ), 'bytes_display_string_dec: 999_999_000 => 999.999 MB' ) ; + is( '999.999 MB', bytes_display_string_dec( 999_999_499 ), 'bytes_display_string_dec: 999_999_499 => 999.999 MB' ) ; + is( '1.000 GB', bytes_display_string_dec( 999_999_500 ), 'bytes_display_string_dec: 999_999_500 => 1.000 GB' ) ; + + is( '1.000 GB', bytes_display_string_dec( 1_000_000_000 ), 'bytes_display_string_dec: 1_000_000_000 => 1.000 GB' ) ; + is( '1.000 GB', bytes_display_string_dec( 1_000_500_000 ), 'bytes_display_string_dec: 1_000_500_000 => 1.000 GB' ) ; + is( '1.001 GB', bytes_display_string_dec( 1_000_500_001 ), 'bytes_display_string_dec: 1_000_501_000 => 1.001 GB' ) ; + is( '999.999 GB', bytes_display_string_dec( 999_999_000_000 ), 'bytes_display_string_dec: 999_999_000_000 => 999.999 GB' ) ; + is( '999.999 GB', bytes_display_string_dec( 999_999_499_999 ), 'bytes_display_string_dec: 999_999_499_999 => 999.999 GB' ) ; + is( '1.000 TB', bytes_display_string_dec( 999_999_500_000 ), 'bytes_display_string_dec: 999_999_500_000 => 1.000 TB' ) ; + + is( '1.000 TB', bytes_display_string_dec( 1_000_000_000_000 ), 'bytes_display_string_dec: 1_000_000_000_000 => 1.000 TB' ) ; + is( '1.000 TB', bytes_display_string_dec( 1_000_500_000_000 ), 'bytes_display_string_dec: 1_000_500_000_000 => 1.000 TB' ) ; + is( '1.001 TB', bytes_display_string_dec( 1_000_500_000_001 ), 'bytes_display_string_dec: 1_000_500_000_000 => 1.000 TB' ) ; + is( '999.999 TB', bytes_display_string_dec( 999_999_000_000_000 ), 'bytes_display_string_dec: 999_999_000_000_000 => 999.999 TB' ) ; + is( '999.999 TB', bytes_display_string_dec( 999_999_499_999_999 ), 'bytes_display_string_dec: 999_999_499_999_999 => 999.999 TB' ) ; + is( '1.000 PB', bytes_display_string_dec( 999_999_500_000_000 ), 'bytes_display_string_dec: 999_999_500_000_000 => 1.000 PB' ) ; + + is( '3.000 GB', bytes_display_string_dec( 3_000_000_000 ), 'bytes_display_string_dec: 3_000_000_000 => 3.000 GB' ) ; + + note( 'Leaving tests_bytes_display_string_dec()' ) ; + return ; +} + +sub bytes_display_string_dec +{ + my ( $bytes ) = @_ ; + + my $readable_value = q{} ; + + if ( ! defined( $bytes ) ) { + return( 'NA' ) ; + } + + if ( not match_number( $bytes ) ) { + return( 'NA' ) ; + } + + SWITCH: { + if ( abs( $bytes ) < ( 1000 ) ) { + $readable_value = mysprintf( '%.0f bytes', $bytes ) ; + last SWITCH ; + } + if ( abs( $bytes ) < ( 1000**2 ) ) { + $readable_value = mysprintf( '%.3f KB', $bytes / 1000 ) ; + last SWITCH ; + } + if ( abs( $bytes ) < ( 999_999_500 ) ) { + $readable_value = mysprintf( '%.3f MB', $bytes / ( 1000**2 ) ) ; + last SWITCH ; + } + if ( abs( $bytes ) < ( 999_999_500_000 ) ) { + $readable_value = mysprintf( '%.3f GB', $bytes / ( 1000**3 ) ) ; + last SWITCH ; + } + if ( abs( $bytes ) < ( 999_999_500_000_000 ) ) { + $readable_value = mysprintf( '%.3f TB', $bytes / ( 1000**4 ) ) ; + last SWITCH ; + } else { + $readable_value = mysprintf( '%.3f PB', $bytes / ( 1000**5 ) ) ; + } + # if you have exabytes (EiB) of email to transfer, you have too much email! + } + #myprint( "$bytes = $readable_value\n" ) ; + + return( $readable_value ) ; +} + + +sub tests_useheader_suggestion +{ + note( 'Entering tests_useheader_suggestion()' ) ; + + is( undef, useheader_suggestion( ), 'useheader_suggestion: no args => undef' ) ; + my $mysync = {} ; + + $mysync->{ h1_nb_msg_noheader } = 0 ; + is( q{}, useheader_suggestion( $mysync ), 'useheader_suggestion: h1_nb_msg_noheader count null => no suggestion' ) ; + $mysync->{ h1_nb_msg_noheader } = 2 ; + is( q{in order to sync those 2 unidentified messages, add option --addheader}, useheader_suggestion( $mysync ), + 'useheader_suggestion: h1_nb_msg_noheader count 2 => suggestion of --addheader' ) ; + + note( 'Leaving tests_useheader_suggestion()' ) ; + return ; +} + +sub useheader_suggestion +{ + my $mysync = shift ; + if ( ! defined $mysync->{ h1_nb_msg_noheader } ) + { + return ; + } + elsif ( 1 <= $mysync->{ h1_nb_msg_noheader } ) + { + return qq{in order to sync those $mysync->{ h1_nb_msg_noheader } unidentified messages, add option --addheader} ; + } + else + { + return q{} ; + } + return ; +} + +sub do_and_print_stats +{ + my $mysync = shift ; + + if ( ! $mysync->{can_do_stats} ) { + return ; + } + + my $timeend = time ; + my $timediff = $timeend - $mysync->{timestart} ; + + my $timeend_str = localtimez( $timeend ) ; + + my $cpu_time = cpu_time( $mysync ) ; + my $cpu_percent = cpu_percent( $mysync, $cpu_time, $timediff ) ; + my $cpu_percent_global = cpu_percent_global( $mysync, $cpu_percent ) ; + + my $memory_consumption_at_end = memory_consumption( ) || 0 ; + my $memory_consumption_at_start = $mysync->{ memory_consumption_at_start } || 0 ; + my $memory_ratio = ( $mysync->{ biggest_message_transferred } ) ? + mysprintf( '%.1f', $memory_consumption_at_end / $mysync->{ biggest_message_transferred } ) : 'NA' ; + + # my $useheader_suggestion = useheader_suggestion( $mysync ) ; + myprint( "++++ Statistics\n" ) ; + myprint( "Transfer started on : $mysync->{ timestart_str }\n" ) ; + myprint( "Transfer ended on : $timeend_str\n" ) ; + myprintf( "Transfer time : %.1f sec\n", $timediff ) ; + myprint( "Folders synced : $h1_folders_wanted_ct/$h1_folders_wanted_nb synced\n" ) ; + myprint( "Messages transferred : $mysync->{ nb_msg_transferred } " ) ; + myprint( "(could be $nb_msg_skipped_dry_mode without dry mode)" ) if ( $mysync->{dry} ) ; + myprint( "\n" ) ; + myprint( "Messages skipped : $mysync->{ nb_msg_skipped }\n" ) ; + myprint( "Messages found duplicate on host1 : $mysync->{ acc1 }->{ nb_msg_duplicate }\n" ) ; + myprint( "Messages found duplicate on host2 : $mysync->{ acc2 }->{ nb_msg_duplicate }\n" ) ; + myprint( "Messages found crossduplicate on host2 : $mysync->{ h2_nb_msg_crossdup }\n" ) ; + myprint( "Messages void (noheader) on host1 : $mysync->{ h1_nb_msg_noheader } ", useheader_suggestion( $mysync ), "\n" ) ; + myprint( "Messages void (noheader) on host2 : $h2_nb_msg_noheader\n" ) ; + nb_messages_in_1_not_in_2( $mysync ) ; + nb_messages_in_2_not_in_1( $mysync ) ; + myprintf( "Messages found in host1 not in host2 : %s messages\n", $mysync->{ nb_messages_in_1_not_in_2 } ) ; + myprintf( "Messages found in host2 not in host1 : %s messages\n", $mysync->{ nb_messages_in_2_not_in_1 } ) ; + myprint( "Messages deleted on host1 : $mysync->{ acc1 }->{ nb_msg_deleted }\n" ) ; + myprint( "Messages deleted on host2 : $mysync->{ acc2 }->{ nb_msg_deleted }\n" ) ; + myprintf( "Total bytes transferred : %s (%s)\n", + $mysync->{total_bytes_transferred}, + bytes_display_string_bin( $mysync->{total_bytes_transferred} ) ) ; + myprintf( "Total bytes skipped : %s (%s)\n", + $mysync->{ total_bytes_skipped }, + bytes_display_string_bin( $mysync->{ total_bytes_skipped } ) ) ; + $timediff ||= 1 ; # No division per 0 + myprintf("Message rate : %.1f messages/s\n", $mysync->{nb_msg_transferred} / $timediff ) ; + myprintf("Average bandwidth rate : %.1f KiB/s\n", $mysync->{total_bytes_transferred} / $KIBI / $timediff ) ; + myprint( "Reconnections to host1 : $mysync->{imap1}->{IMAPSYNC_RECONNECT_COUNT}\n" ) ; + myprint( "Reconnections to host2 : $mysync->{imap2}->{IMAPSYNC_RECONNECT_COUNT}\n" ) ; + myprintf("Memory consumption at the end : %.1f MiB (started with %.1f MiB)\n", + $memory_consumption_at_end / $KIBI / $KIBI, + $memory_consumption_at_start / $KIBI / $KIBI ) ; + myprint( "Load end is : " . ( join( q{ }, loadavg( ) ) || 'unknown' ), " on $mysync->{cpu_number} cores\n" ) ; + myprint( "CPU time and %cpu : $cpu_time sec $cpu_percent %cpu $cpu_percent_global %allcpus\n" ) ; + myprintf("Biggest message transferred : %s bytes (%s)\n", + $mysync->{ biggest_message_transferred }, + bytes_display_string_bin( $mysync->{ biggest_message_transferred } ) ) ; + myprint( "Memory/biggest message ratio : $memory_ratio\n" ) ; + if ( $mysync->{ foldersizesatend } and $mysync->{ foldersizes } ) { + + + my $nb_msg_start_diff = diff_or_NA( $mysync->{ h2_nb_msg_start }, $mysync->{ h1_nb_msg_start } ) ; + my $bytes_start_diff = diff_or_NA( $mysync->{ h2_bytes_start }, $mysync->{ h1_bytes_start } ) ; + + myprintf("Start difference host2 - host1 : %s messages, %s bytes (%s)\n", $nb_msg_start_diff, + $bytes_start_diff, + bytes_display_string_bin( $bytes_start_diff ) ) ; + + my $nb_msg_end_diff = diff_or_NA( $h2_nb_msg_end, $h1_nb_msg_end ) ; + my $bytes_end_diff = diff_or_NA( $h2_bytes_end, $h1_bytes_end ) ; + + myprintf("Final difference host2 - host1 : %s messages, %s bytes (%s)\n", $nb_msg_end_diff, + $bytes_end_diff, + bytes_display_string_bin( $bytes_end_diff ) ) ; + } + + comment_on_final_diff_in_1_not_in_2( $mysync ) ; + comment_on_final_diff_in_2_not_in_1( $mysync ) ; + myprint( "Detected $mysync->{nb_errors} errors\n\n" ) ; + + myprint( $mysync->{ warn_release }, "\n" ) ; + myprint( homepage( ), "\n" ) ; + return ; +} + +sub diff_or_NA +{ + my( $n1, $n2 ) = @ARG ; + + if ( not defined $n1 or not defined $n2 ) { + return 'NA' ; + } + + if ( not match_number( $n1 ) + or not match_number( $n2 ) ) { + return 'NA' ; + } + + return( $n1 - $n2 ) ; +} + +sub match_number +{ + my $n = shift @ARG ; + + if ( not defined $n ) { + return 0 ; + } + if ( $n =~ /[0-9]+\.?[0-9]?/x ) { + return 1 ; + } + else { + return 0 ; + } +} + + +sub tests_match_number +{ + note( 'Entering tests_match_number()' ) ; + + + is( 0, match_number( ), 'match_number: no parameters => 0' ) ; + is( 0, match_number( undef ), 'match_number: undef => 0' ) ; + is( 0, match_number( 'blabla' ), 'match_number: blabla => 0' ) ; + is( 1, match_number( 0 ), 'match_number: 0 => 1' ) ; + is( 1, match_number( 1 ), 'match_number: 1 => 1' ) ; + is( 1, match_number( 1.0 ), 'match_number: 1.0 => 1' ) ; + is( 1, match_number( 0.0 ), 'match_number: 0.0 => 1' ) ; + + note( 'Leaving tests_match_number()' ) ; + return ; +} + + + +sub tests_diff_or_NA +{ + note( 'Entering tests_diff_or_NA()' ) ; + + + is( 'NA', diff_or_NA( ), 'diff_or_NA: no parameters => NA' ) ; + is( 'NA', diff_or_NA( undef ), 'diff_or_NA: undef => NA' ) ; + is( 'NA', diff_or_NA( undef, undef ), 'diff_or_NA: undef undef => NA' ) ; + is( 'NA', diff_or_NA( undef, 1 ), 'diff_or_NA: undef 1 => NA' ) ; + is( 'NA', diff_or_NA( 1, undef ), 'diff_or_NA: 1 undef => NA' ) ; + is( 'NA', diff_or_NA( 'blabla', 1 ), 'diff_or_NA: blabla 1 => NA' ) ; + is( 'NA', diff_or_NA( 1, 'blabla' ), 'diff_or_NA: 1 blabla => NA' ) ; + is( 0, diff_or_NA( 1, 1 ), 'diff_or_NA: 1 1 => 0' ) ; + is( 1, diff_or_NA( 1, 0 ), 'diff_or_NA: 1 0 => 1' ) ; + is( -1, diff_or_NA( 0, 1 ), 'diff_or_NA: 0 1 => -1' ) ; + is( 0, diff_or_NA( 1.0, 1 ), 'diff_or_NA: 1.0 1 => 0' ) ; + is( 1, diff_or_NA( 1.0, 0 ), 'diff_or_NA: 1.0 0 => 1' ) ; + is( -1, diff_or_NA( 0, 1.0 ), 'diff_or_NA: 0 1.0 => -1' ) ; + + note( 'Leaving tests_diff_or_NA()' ) ; + return ; +} + +sub homepage +{ + return( 'Homepage: https://imapsync.lamiral.info/' ) ; +} + + +sub load_modules +{ + if ( $sync->{ssl1} + or $sync->{ssl2} + or $sync->{tls1} + or $sync->{tls2}) { + if ( $sync->{inet4} ) { + IO::Socket::SSL->import( 'inet4' ) ; + } + if ( $sync->{inet6} ) { + IO::Socket::SSL->import( 'inet6' ) ; + } + } + return ; +} + + +# Globals: $skipsize $wholeheaderifneeded +sub parse_header_msg +{ + my ( $mysync, $imap, $m_uid, $s_heads, $s_fir, $side, $s_hash ) = @_ ; + + my $head = $s_heads->{$m_uid} ; + my $headnum = scalar keys %{ $head } ; + $mysync->{ debug } and myprint( "$side: uid $m_uid number of headers, pass one: ", $headnum, "\n" ) ; + + if ( ( ! $headnum ) and ( $wholeheaderifneeded ) ){ + $mysync->{ debug } and myprint( "$side: uid $m_uid no header by parse_headers so taking whole header with BODY.PEEK[HEADER]\n" ) ; + $imap->fetch($m_uid, 'BODY.PEEK[HEADER]' ) ; + my $whole_header = $imap->_transaction_literals ; + + #myprint( $whole_header ) ; + $head = decompose_header( $whole_header ) ; + + $headnum = scalar keys %{ $head } ; + $mysync->{ debug } and myprint( "$side: uid $m_uid number of headers, pass two: ", $headnum, "\n" ) ; + } + + #myprint( Data::Dumper->Dump( [ $head, \%useheader ] ) ) ; + + my $headstr = header_construct( $mysync, $head, $side, $m_uid ) ; + + if ( ( ! $headstr ) and ( $mysync->{addheader} ) and ( $side eq 'Host1' ) ) { + my $header = add_header( $m_uid ) ; + $mysync->{ debug } and myprint( "$side: uid $m_uid no header found so adding our own [$header]\n" ) ; + $headstr .= uc $header ; + $s_fir->{$m_uid}->{NO_HEADER} = 1; + } + + return if ( ! $headstr ) ; + + my $size = $s_fir->{$m_uid}->{'RFC822.SIZE'} ; + my $flags = $s_fir->{$m_uid}->{'FLAGS'} ; + my $idate = $s_fir->{$m_uid}->{'INTERNALDATE'} ; + $size = length $headstr unless ( $size ) ; + my $m_md5 = md5_base64( $headstr ) ; + + my $key ; + if ( $skipsize ) { + $key = "$m_md5"; + } + else { + $key = "$m_md5:$size"; + } + + if ( exists $s_hash->{"$key"} ) + { + # 0 return code is used to identify duplicate message hash + my $dup_ref = $s_hash->{"$key"}->{'U'} ; + my $num = scalar( @{ $dup_ref } ) ; + push( @{ $dup_ref }, $m_uid ) ; + my $keydup = "$key#$num" ; + $mysync->{ debug } and myprint( "$side: uid $m_uid sig $keydup size $size idate $idate dup @{ $dup_ref }\n" ) ; + if ( $mysync->{ syncduplicates } ) + { + $s_hash->{"$keydup"}{'5'} = $m_md5 ; + $s_hash->{"$keydup"}{'s'} = $size ; + $s_hash->{"$keydup"}{'D'} = $idate ; + $s_hash->{"$keydup"}{'F'} = $flags ; + $s_hash->{"$keydup"}{'m'} = $m_uid ; + } + return 0 ; + } + else + { + $s_hash->{"$key"}{'5'} = $m_md5 ; + $s_hash->{"$key"}{'s'} = $size ; + $s_hash->{"$key"}{'D'} = $idate ; + $s_hash->{"$key"}{'F'} = $flags ; + $s_hash->{"$key"}{'m'} = $m_uid ; + $s_hash->{"$key"}{'U'} = [ $m_uid ] ; # ? or [ ] ? + $mysync->{ debug } and myprint( "$side: uid $m_uid sig $key size $size idate $idate\n" ) ; + return( 1 ) ; + } + + # we should not be here + return ; +} + +sub tests_header_construct +{ + note( 'Entering tests_header_construct()' ) ; + + is( undef, header_construct( ), 'header_construct: no args => undef' ) ; + my $mysync = {} ; + my $head = { + 'key1' => [ 'val1_key1' ] + } ; + is( undef, header_construct( $mysync, $head, 'Host1', '1' ), 'header_construct: key1 val1_key1 no useheader => undef' ) ; + + $mysync->{useheader}->{ 'KEY1' } = 1 ; + is( 'KEY1: VAL1_KEY1', header_construct( $mysync, $head, 'Host1', '1' ), 'header_construct: key1 val1_key1 => KEY1: VAL1_KEY1' ) ; + + + + $head = { + 'key1' => [ 'val1_key1', 'val3_key1', 'val2_key1' ] + } ; + is( 'KEY1: VAL1_KEY1KEY1: VAL2_KEY1KEY1: VAL3_KEY1', header_construct( $mysync, $head, 'Host1', '1' ), + 'header_construct: key1 val1_key1 val3_key1 val2_key1 => KEY1: VAL1_KEY1KEY1: VAL2_KEY1KEY1: VAL3_KEY1' ) ; + + $head = { + 'key1' => [ 'val1_key1', 'val3_key1', ' val2_key1' ] + } ; + is( 'KEY1: VAL1_KEY1KEY1: VAL2_KEY1KEY1: VAL3_KEY1', header_construct( $mysync, $head, 'Host1', '1' ), + 'header_construct: key1 val1_key1 val3_key1 val2_key1 => KEY1: VAL1_KEY1KEY1: VAL2_KEY1KEY1: VAL3_KEY1' ) ; + + $mysync->{useheader}->{ 'ALL' } = 1 ; + + is( 'KEY1: VAL1_KEY1KEY1: VAL2_KEY1KEY1: VAL3_KEY1', header_construct( $mysync, $head, 'Host1', '1' ), + 'header_construct: key1 val1_key1 val3_key1 val2_key1 useheader ALL => KEY1: VAL1_KEY1KEY1: VAL2_KEY1KEY1: VAL3_KEY1' ) ; + + $mysync->{skipheader} = 'key1' ; + is( undef, header_construct( $mysync, $head, 'Host1', '1' ), + 'header_construct: key1 val1_key1 val3_key1 val2_key1 useheader ALL => undef' ) ; + + $head = { + 'key1' => [ 'val1_key1', 'val3_key1', ' val2_key1' ], + 'key2' => [ 'val1_key2', 'val3_key2', ' val2_key2' ] + } ; + is( 'KEY2: VAL1_KEY2KEY2: VAL2_KEY2KEY2: VAL3_KEY2', header_construct( $mysync, $head, 'Host1', '1' ), + 'header_construct: ... useheader ALL skipheader key1 => KEY2: VAL1_KEY2KEY2: VAL2_KEY2KEY2: VAL3_KEY2' ) ; + + + note( 'Leaving tests_header_construct()' ) ; + return ; +} + + +# No global in header_construct +sub header_construct +{ + my( $mysync, $head, $side, $m_uid ) = @_ ; + + my @headstr ; + foreach my $h ( sort keys %{ $head } ) { + next if ( not ( exists $mysync->{useheader}->{ uc $h } ) + and ( not exists $mysync->{useheader}->{ 'ALL' } ) + ) ; + foreach my $val ( @{$head->{$h}} ) { + + my $H = header_line_normalize( $h, $val ) ; + + # show stuff in debug mode + $mysync->{ debug } and myprint( "$side uid $m_uid header [$H]", "\n" ) ; + + if ( $mysync->{skipheader} and $H =~ m/$mysync->{skipheader}/xi) { + $mysync->{ debug } and myprint( "$side uid $m_uid skipping header [$H]\n" ) ; + next ; + } + push @headstr, $H ; + } + } + my $headstr = join( '', sort @headstr ) || undef ; + return( $headstr ) ; +} + + +sub header_line_normalize +{ + my( $header_key, $header_val ) = @_ ; + + # no 8-bit data in headers ! + $header_val =~ s/[\x80-\xff]/X/xog; + + # change tabulations to space (Gmail bug on with "Received:" on multilines) + $header_val =~ s/\t/\ /xgo ; + + # remove the first blanks ( dbmail bug? ) + $header_val =~ s/^\s*//xo; + + # remove the last blanks ( Gmail bug ) + $header_val =~ s/\s*$//xo; + + # remove successive blanks ( Mailenable does it ) + $header_val =~ s/\s+/ /xgo; + + # remove Message-Id value domain part ( Mailenable changes it ) + if ( ( $messageidnodomain ) and ( 'MESSAGE-ID' eq uc $header_key ) ) { $header_val =~ s/^([^@]+).*$/$1/xo ; } + + # and uppercase header line + # (dbmail and dovecot) + + my $header_line = uc "$header_key: $header_val" ; + + return( $header_line ) ; +} + +sub tests_header_line_normalize +{ + note( 'Entering tests_header_line_normalize()' ) ; + + + ok( ': ' eq header_line_normalize( q{}, q{} ), 'header_line_normalize: empty args' ) ; + ok( 'HHH: VVV' eq header_line_normalize( 'hhh', 'vvv' ), 'header_line_normalize: hhh vvv ' ) ; + ok( 'HHH: VVV' eq header_line_normalize( 'hhh', ' vvv' ), 'header_line_normalize: remove first blancs' ) ; + ok( 'HHH: AA BB CCC D' eq header_line_normalize( 'hhh', 'aa bb ccc d' ), 'header_line_normalize: remove succesive blanks' ) ; + ok( 'HHH: AA BB CCC' eq header_line_normalize( 'hhh', 'aa bb ccc ' ), 'header_line_normalize: remove last blanks' ) ; + ok( 'HHH: VVV XX YY' eq header_line_normalize( 'hhh', "vvv\t\txx\tyy" ), 'header_line_normalize: tabs' ) ; + ok( 'HHH: XABX' eq header_line_normalize( 'hhh', "\x80AB\xff" ), 'header_line_normalize: 8bit' ) ; + + note( 'Leaving tests_header_line_normalize()' ) ; + return ; +} + + +sub tests_firstline +{ + note( 'Entering tests_firstline()' ) ; + + is( q{}, firstline( 'W/tmp/tests/noexist.txt' ), 'firstline: getting empty string from inexisting W/tmp/tests/noexist.txt' ) ; + + ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'firstline: mkpath W/tmp/tests/' ) ; + + is( "blabla\n" , string_to_file( "blabla\n", 'W/tmp/tests/firstline.txt' ), 'firstline: put blabla in W/tmp/tests/firstline.txt' ) ; + is( 'blabla' , firstline( 'W/tmp/tests/firstline.txt' ), 'firstline: get blabla from W/tmp/tests/firstline.txt' ) ; + + is( q{} , string_to_file( q{}, 'W/tmp/tests/firstline2.txt' ), 'firstline: put empty string in W/tmp/tests/firstline2.txt' ) ; + is( q{} , firstline( 'W/tmp/tests/firstline2.txt' ), 'firstline: get empty string from W/tmp/tests/firstline2.txt' ) ; + + is( "\n" , string_to_file( "\n", 'W/tmp/tests/firstline3.txt' ), 'firstline: put CR in W/tmp/tests/firstline3.txt' ) ; + is( q{} , firstline( 'W/tmp/tests/firstline3.txt' ), 'firstline: get empty string from W/tmp/tests/firstline3.txt' ) ; + + is( "blabla\nTiti\n" , string_to_file( "blabla\nTiti\n", 'W/tmp/tests/firstline4.txt' ), 'firstline: put blabla\nTiti\n in W/tmp/tests/firstline4.txt' ) ; + is( 'blabla' , firstline( 'W/tmp/tests/firstline4.txt' ), 'firstline: get blabla from W/tmp/tests/firstline4.txt' ) ; + + note( 'Leaving tests_firstline()' ) ; + return ; +} + +sub firstline +{ + # extract the first line of a file (without \n) + # return empty string if error or empty string + + my $file = shift ; + my $line ; + + $line = nthline( $file, 1 ) ; + return $line ; +} + + + +sub tests_secondline +{ + note( 'Entering tests_secondline()' ) ; + + is( q{}, secondline( 'W/tmp/tests/noexist.txt' ), 'secondline: getting empty string from inexisting W/tmp/tests/noexist.txt' ) ; + is( q{}, secondline( 'W/tmp/tests/noexist.txt', 2 ), 'secondline: 2nd getting empty string from inexisting W/tmp/tests/noexist.txt' ) ; + + ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'secondline: mkpath W/tmp/tests/' ) ; + + is( "L1\nL2\nL3\nL4\n" , string_to_file( "L1\nL2\nL3\nL4\n", 'W/tmp/tests/secondline.txt' ), 'secondline: put L1\nL2\nL3\nL4\n in W/tmp/tests/secondline.txt' ) ; + is( 'L2' , secondline( 'W/tmp/tests/secondline.txt' ), 'secondline: get L2 from W/tmp/tests/secondline.txt' ) ; + + + note( 'Leaving tests_secondline()' ) ; + return ; +} + + +sub secondline +{ + # extract the second line of a file (without \n) + # return empty string if error or empty string + + my $file = shift ; + my $line ; + + $line = nthline( $file, 2 ) ; + return $line ; +} + + + + +sub tests_nthline +{ + note( 'Entering tests_nthline()' ) ; + + is( q{}, nthline( 'W/tmp/tests/noexist.txt' ), 'nthline: getting empty string from inexisting W/tmp/tests/noexist.txt' ) ; + is( q{}, nthline( 'W/tmp/tests/noexist.txt', 2 ), 'nthline: 2nd getting empty string from inexisting W/tmp/tests/noexist.txt' ) ; + + ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'nthline: mkpath W/tmp/tests/' ) ; + is( "L1\nL2\nL3\nL4\n" , string_to_file( "L1\nL2\nL3\nL4\n", 'W/tmp/tests/nthline.txt' ), 'nthline: put L1\nL2\nL3\nL4\n in W/tmp/tests/nthline.txt' ) ; + is( 'L3' , nthline( 'W/tmp/tests/nthline.txt', 3 ), 'nthline: get L3 from W/tmp/tests/nthline.txt' ) ; + + + note( 'Leaving tests_nthline()' ) ; + return ; +} + + +sub nthline +{ + # extract the nth line of a file (without \n) + # return empty string if error or empty string + + my $file = shift ; + my $num = shift ; + + if ( ! all_defined( $file, $num ) ) { return q{} ; } + + my $line ; + + $line = ( file_to_array( $file ) )[$num - 1] ; + if ( ! defined $line ) + { + return q{} ; + } + else + { + chomp $line ; + return $line ; + } +} + +sub tests_file_to_array +{ + note( 'Entering tests_file_to_array()' ) ; + + is( undef, file_to_array( ), 'file_to_array: no args => undef' ) ; + is( undef, file_to_array( '/noexist' ), 'file_to_array: /noexist => undef' ) ; + is( undef, file_to_array( '/' ), 'file_to_array: reading a directory => undef' ) ; + + ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'file_to_array: mkpath W/tmp/tests/' ) ; + is( "L1\nL2\nL3\nL4\n" , string_to_file( "L1\nL2\nL3\nL4\n", 'W/tmp/tests/file_to_array.txt' ), 'file_to_array: put L1\nL2\nL3\nL4\n in W/tmp/tests/file_to_array.txt' ) ; + is_deeply( [ "L1\n", "L2\n", "L3\n", "L4\n" ] , [ file_to_array( 'W/tmp/tests/file_to_array.txt' ) ], 'file_to_array: get back L1\n L2\n L3\n L4\n from W/tmp/tests/file_to_array.txt' ) ; + + note( 'Leaving tests_file_to_array()' ) ; + return ; +} + +sub file_to_array +{ + + my( $file ) = shift ; + if ( ! $file ) { return ; } + if ( ! -e $file ) { return ; } + if ( ! -f $file ) { return ; } + if ( ! -r $file ) { return ; } + + my @string ; + + if ( open my $FILE, '<', $file ) + { + @string = <$FILE> ; + close $FILE ; + return( @string ) ; + } + else + { + myprint( "Error reading file $file : $OS_ERROR\n" ) ; + return ; + } +} + + +sub tests_file_to_string +{ + note( 'Entering tests_file_to_string()' ) ; + + is( undef, file_to_string( ), 'file_to_string: no args => undef' ) ; + is( undef, file_to_string( '/noexist' ), 'file_to_string: /noexist => undef' ) ; + is( undef, file_to_string( '/' ), 'file_to_string: reading a directory => undef' ) ; + ok( file_to_string( $PROGRAM_NAME ), 'file_to_string: reading myself' ) ; + + ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'file_to_string: mkpath W/tmp/tests/' ) ; + + is( 'lilili', string_to_file( 'lilili', 'W/tmp/tests/canbewritten' ), 'file_to_string: string_to_file filling W/tmp/tests/canbewritten with lilili' ) ; + is( 'lilili', file_to_string( 'W/tmp/tests/canbewritten' ), 'file_to_string: reading W/tmp/tests/canbewritten is lilili' ) ; + + is( q{}, string_to_file( q{}, 'W/tmp/tests/empty' ), 'file_to_string: string_to_file filling W/tmp/tests/empty with empty string' ) ; + is( q{}, file_to_string( 'W/tmp/tests/empty' ), 'file_to_string: reading W/tmp/tests/empty is empty' ) ; + + note( 'Leaving tests_file_to_string()' ) ; + return ; +} + +sub file_to_string +{ + my $file = shift ; + if ( ! $file ) { return ; } + if ( ! -e $file ) { return ; } + if ( ! -f $file ) { return ; } + if ( ! -r $file ) { return ; } + + return( join q{}, file_to_array( $file ) ) ; +} + + +sub tests_string_to_file +{ + note( 'Entering tests_string_to_file()' ) ; + + is( undef, string_to_file( ), 'string_to_file: no args => undef' ) ; + is( undef, string_to_file( 'lalala' ), 'string_to_file: one arg => undef' ) ; + is( undef, string_to_file( 'lalala', '.' ), 'string_to_file: writing a directory => undef' ) ; + ok( (-d 'W/tmp/tests/' or mkpath( 'W/tmp/tests/' ) ), 'string_to_file: mkpath W/tmp/tests/' ) ; + is( 'lalala', string_to_file( 'lalala', 'W/tmp/tests/canbewritten' ), 'string_to_file: W/tmp/tests/canbewritten with lalala' ) ; + is( q{}, string_to_file( q{}, 'W/tmp/tests/empty' ), 'string_to_file: W/tmp/tests/empty with empty string' ) ; + + SKIP: { + Readonly my $NB_UNX_tests_string_to_file => 1 ; + skip( 'Not on Unix non-root', $NB_UNX_tests_string_to_file ) if ('MSWin32' eq $OSNAME or '0' eq $EFFECTIVE_USER_ID ) ; + is( undef, string_to_file( 'lalala', '/cantouch' ), 'string_to_file: /cantouch denied => undef' ) ; + } + + note( 'Leaving tests_string_to_file()' ) ; + return ; +} + +sub string_to_file +{ + my( $string, $file ) = @_ ; + if( ! defined $string ) { return ; } + if( ! defined $file ) { return ; } + + if ( ! -e $file && ! -w dirname( $file ) ) { + myprint( "string_to_file: directory of $file is not writable\n" ) ; + return ; + } + + if ( ! sysopen( FILE, $file, O_WRONLY|O_TRUNC|O_CREAT, 0600) ) { + myprint( "string_to_file: failure writing to $file with error: $OS_ERROR\n" ) ; + return ; + } + print FILE $string ; + close FILE ; + return $string ; +} + +0 and <<'MULTILINE_COMMENT' ; +This is a multiline comment. +Based on David Carter discussion, to do: +* Call parameters stay the same. +* Now always "return( $string, $error )". Descriptions below. +OK * Still capture STDOUT via "1> $output_tmpfile" to finish in $string and "return( $string, $error )" +OK * Now also capture STDERR via "2> $error_tmpfile" to finish in $error and "return( $string, $error )" +OK * in case of CHILD_ERROR, return( undef, $error ) + and print $error, with folder/UID/maybeSubject context, + on console and at the end with the final error listing. Count this as a sync error. +* in case of good command, take final $string as is, unless void. In case $error with value then print it. +* in case of good command and final $string empty, consider it like CHILD_ERROR => + return( undef, $error ) and print $error, with folder/UID/maybeSubject context, + on console and at the end with the final error listing. Count this as a sync error. +MULTILINE_COMMENT +# End of multiline comment. + +sub pipemess +{ + my ( $string, @commands ) = @_ ; + my $error = q{} ; + foreach my $command ( @commands ) { + my $input_tmpfile = "$sync->{ tmpdir }/imapsync_tmp_file.$PROCESS_ID.inp.txt" ; + my $output_tmpfile = "$sync->{ tmpdir }/imapsync_tmp_file.$PROCESS_ID.out.txt" ; + my $error_tmpfile = "$sync->{ tmpdir }/imapsync_tmp_file.$PROCESS_ID.err.txt" ; + string_to_file( $string, $input_tmpfile ) ; + ` $command < $input_tmpfile 1> $output_tmpfile 2> $error_tmpfile ` ; + my $is_command_ko = $CHILD_ERROR ; + my $error_cmd = file_to_string( $error_tmpfile ) ; + chomp( $error_cmd ) ; + $string = file_to_string( $output_tmpfile ) ; + my $string_len = length( $string ) ; + unlink $input_tmpfile, $output_tmpfile, $error_tmpfile ; + + if ( $is_command_ko or ( ! $string_len ) ) { + my $cmd_exit_value = $CHILD_ERROR >> 8 ; + my $cmd_end_signal = $CHILD_ERROR & 127 ; + my $signal_log = ( $cmd_end_signal ) ? " signal $cmd_end_signal and" : q{} ; + my $error_log = qq{Failure: --pipemess command "$command" ended with$signal_log "$string_len" characters exit value "$cmd_exit_value" and STDERR "$error_cmd"\n} ; + myprint( $error_log ) ; + if ( wantarray ) { + return @{ [ undef, $error_log ] } + }else{ + return ; + } + } + if ( $error_cmd ) { + $error .= qq{STDERR of --pipemess "$command": $error_cmd\n} ; + myprint( qq{STDERR of --pipemess "$command": $error_cmd\n} ) ; + } + } + #myprint( "[$string]\n" ) ; + if ( wantarray ) { + return ( $string, $error ) ; + }else{ + return $string ; + } +} + + + +sub tests_pipemess +{ + note( 'Entering tests_pipemess()' ) ; + + + SKIP: { + Readonly my $NB_WIN_tests_pipemess => 3 ; + skip( 'Not on MSWin32', $NB_WIN_tests_pipemess ) if ('MSWin32' ne $OSNAME) ; + # Windows + # "type" command does not accept redirection of STDIN with < + # "sort" does + ok( "nochange\n" eq pipemess( 'nochange', 'sort' ), 'pipemess: nearly no change by sort' ) ; + ok( "nochange2\n" eq pipemess( 'nochange2', qw( sort sort ) ), 'pipemess: nearly no change by sort,sort' ) ; + # command not found + #diag( 'Warning and failure about cacaprout are on purpose' ) ; + ok( ! defined( pipemess( q{}, 'cacaprout' ) ), 'pipemess: command not found' ) ; + + } ; + + my ( $stringT, $errorT ) ; + + SKIP: { + Readonly my $NB_UNX_tests_pipemess => 25 ; + skip( 'Not on Unix', $NB_UNX_tests_pipemess ) if ('MSWin32' eq $OSNAME) ; + # Unix + ok( 'nochange' eq pipemess( 'nochange', 'cat' ), 'pipemess: no change by cat' ) ; + + ok( 'nochange2' eq pipemess( 'nochange2', 'cat', 'cat' ), 'pipemess: no change by cat,cat' ) ; + + ok( " 1\tnumberize\n" eq pipemess( "numberize\n", 'cat -n' ), 'pipemess: numberize by cat -n' ) ; + ok( " 1\tnumberize\n 2\tnumberize\n" eq pipemess( "numberize\nnumberize\n", 'cat -n' ), 'pipemess: numberize by cat -n' ) ; + + ok( "A\nB\nC\n" eq pipemess( "A\nC\nB\n", 'sort' ), 'pipemess: sort' ) ; + + # command not found + #diag( 'Warning and failure about cacaprout are on purpose' ) ; + is( undef, pipemess( q{}, 'cacaprout' ), 'pipemess: command not found' ) ; + + # success with true but no output at all + is( undef, pipemess( q{blabla}, 'true' ), 'pipemess: true but no output' ) ; + + # failure with false and no output at all + is( undef, pipemess( q{blabla}, 'false' ), 'pipemess: false and no output' ) ; + + # Failure since pipemess is not a real pipe, so first cat wait for standard input + is( q{blabla}, pipemess( q{blabla}, '( cat|cat ) ' ), 'pipemess: ok by ( cat|cat )' ) ; + + + ( $stringT, $errorT ) = pipemess( 'nochange', 'cat' ) ; + is( $stringT, 'nochange', 'pipemess: list context, no change by cat, string' ) ; + is( $errorT, q{}, 'pipemess: list context, no change by cat, no error' ) ; + + ( $stringT, $errorT ) = pipemess( 'dontcare', 'true' ) ; + is( $stringT, undef, 'pipemess: list context, true but no output, string' ) ; + like( $errorT, qr{\QFailure: --pipemess command "true" ended with "0" characters exit value "0" and STDERR ""\E}xm, 'pipemess: list context, true but no output, error' ) ; + + ( $stringT, $errorT ) = pipemess( 'dontcare', 'false' ) ; + is( $stringT, undef, 'pipemess: list context, false and no output, string' ) ; + like( $errorT, qr{\QFailure: --pipemess command "false" ended with "0" characters exit value "1" and STDERR ""\E}xm, + 'pipemess: list context, false and no output, error' ) ; + + ( $stringT, $errorT ) = pipemess( 'dontcare', '/bin/echo -n blablabla' ) ; + is( $stringT, q{blablabla}, 'pipemess: list context, "echo -n blablabla", string' ) ; + is( $errorT, q{}, 'pipemess: list context, "echo blablabla", error' ) ; + + + ( $stringT, $errorT ) = pipemess( 'dontcare', '( echo -n blablabla 3>&1 1>&2 2>&3 )' ) ; + is( $stringT, undef, 'pipemess: list context, "no output STDERR blablabla", string' ) ; + like( $errorT, qr{blablabla"}xm, 'pipemess: list context, "no output STDERR blablabla", error' ) ; + + + ( $stringT, $errorT ) = pipemess( 'dontcare', '( echo -n blablabla 3>&1 1>&2 2>&3 )', 'false' ) ; + is( $stringT, undef, 'pipemess: list context, "no output STDERR blablabla then false", string' ) ; + like( $errorT, qr{blablabla"}xm, 'pipemess: list context, "no output STDERR blablabla then false", error' ) ; + + ( $stringT, $errorT ) = pipemess( 'dontcare', 'false', '( echo -n blablabla 3>&1 1>&2 2>&3 )' ) ; + is( $stringT, undef, 'pipemess: list context, "false then STDERR blablabla", string' ) ; + like( $errorT, qr{\QFailure: --pipemess command "false" ended with "0" characters exit value "1" and STDERR ""\E}xm, + 'pipemess: list context, "false then STDERR blablabla", error' ) ; + + ( $stringT, $errorT ) = pipemess( 'dontcare', '( echo rrrrr ; echo -n error_blablabla 3>&1 1>&2 2>&3 )' ) ; + like( $stringT, qr{rrrrr}xm, 'pipemess: list context, "STDOUT rrrrr STDERR error_blablabla", string' ) ; + like( $errorT, qr{STDERR.*error_blablabla}xm, 'pipemess: list context, "STDOUT rrrrr STDERR error_blablabla", error' ) ; + + } + + ( $stringT, $errorT ) = pipemess( 'dontcare', 'cacaprout' ) ; + is( $stringT, undef, 'pipemess: list context, cacaprout not found, string' ) ; + like( $errorT, qr{\QFailure: --pipemess command "cacaprout" ended with "0" characters exit value\E}xm, + 'pipemess: list context, cacaprout not found, error' ) ; + + note( 'Leaving tests_pipemess()' ) ; + return ; +} + + + +sub tests_is_a_release_number +{ + note( 'Entering tests_is_a_release_number()' ) ; + + is( undef, is_a_release_number( ), 'is_a_release_number: no args => undef' ) ; + ok( is_a_release_number( $RELEASE_NUMBER_EXAMPLE_1 ), 'is_a_release_number 1.351' ) ; + ok( is_a_release_number( $RELEASE_NUMBER_EXAMPLE_2 ), 'is_a_release_number 42.4242' ) ; + ok( is_a_release_number( imapsync_version( $sync ) ), 'is_a_release_number imapsync_version( )' ) ; + ok( ! is_a_release_number( 'blabla' ), '! is_a_release_number blabla' ) ; + + note( 'Leaving tests_is_a_release_number()' ) ; + return ; +} + +sub is_a_release_number +{ + my $number = shift ; + if ( ! defined $number ) { return ; } + return( $number =~ m{^\d+\.\d+$}xo ) ; +} + + + +sub imapsync_version_public +{ + + my $local_version = imapsync_version( $sync ) ; + my $imapsync_basename = imapsync_basename( ) ; + my $context = imapsync_context( ) ; + my $agent_info = "$OSNAME system, perl " + . mysprintf( '%vd', $PERL_VERSION) + . ", Mail::IMAPClient $Mail::IMAPClient::VERSION" + . " $imapsync_basename" + . " $context" ; + my $sock = IO::Socket::INET->new( + PeerAddr => 'imapsync.lamiral.info', + PeerPort => 80, + Proto => 'tcp', + ) ; + return( 'unknown' ) if not $sock ; + print $sock + "GET /prj/imapsync/VERSION HTTP/1.0\r\n", + "User-Agent: imapsync/$local_version ($agent_info)\r\n", + "Host: ks.lamiral.info\r\n\r\n" ; + my @line = <$sock> ; + close $sock ; + my $last_release = $line[$LAST] ; + chomp $last_release ; + return( $last_release ) ; +} + +sub not_long_imapsync_version_public +{ + #myprint( "Entering not_long_imapsync_version_public\n" ) ; + + my $fake = shift ; + if ( $fake ) { return $fake } + + my $val ; + + # Doesn't work with gethostbyname (see perlipc) + #local $SIG{ALRM} = sub { die "alarm\n" } ; + + if ('MSWin32' eq $OSNAME) { + local $SIG{ALRM} = sub { die "alarm\n" } ; + }else{ + + POSIX::sigaction(SIGALRM, + POSIX::SigAction->new(sub { croak 'alarm' } ) ) + or myprint( "Error setting SIGALRM handler: $OS_ERROR\n" ) ; + } + + my $ret = eval { + alarm 3 ; + { + $val = imapsync_version_public( ) ; + #sleep 4 ; + #myprint( "End of imapsync_version_public\n" ) ; + } + alarm 0 ; + 1 ; + } ; + #myprint( "eval [$ret]\n" ) ; + if ( ( not $ret ) or $EVAL_ERROR ) { + #myprint( "$EVAL_ERROR" ) ; + if ($EVAL_ERROR =~ /alarm/) { + # timed out + return('timeout') ; + }else{ + alarm 0 ; + return( 'unknown' ) ; # propagate unexpected errors + } + }else { + # Good! + return( $val ) ; + } +} + +sub tests_not_long_imapsync_version_public +{ + note( 'Entering tests_not_long_imapsync_version_public()' ) ; + + + is( 1, is_a_release_number( not_long_imapsync_version_public( ) ), + 'not_long_imapsync_version_public: public release is a number' ) ; + + note( 'Leaving tests_not_long_imapsync_version_public()' ) ; + return ; +} + +sub check_last_release +{ + my $fake = shift ; + my $public_release = not_long_imapsync_version_public( $fake ) ; + $sync->{ debug } and myprint( "check_last_release: [$public_release]\n" ) ; + my $inline_help_when_on = '( Use --noreleasecheck to avoid this release check. )' ; + + if ( $public_release eq 'unknown' ) { + return( 'Imapsync public release is unknown.' . $inline_help_when_on ) ; + } + + if ( $public_release eq 'timeout' ) { + return( 'Imapsync public release is unknown (timeout).' . $inline_help_when_on ) ; + } + + if ( ! is_a_release_number( $public_release ) ) { + return( "Imapsync public release is unknown ($public_release)." . $inline_help_when_on ) ; + } + + my $imapsync_here = imapsync_version( $sync ) ; + + if ( $public_release > $imapsync_here ) { + return( 'This imapsync is not up to date. ' . "( local $imapsync_here < official $public_release )" . $inline_help_when_on ) ; + }else{ + return( 'This imapsync is up to date. ' . "( local $imapsync_here >= official $public_release )" . $inline_help_when_on ) ; + } + + return( 'really unknown' ) ; # Should never arrive here +} + +sub tests_check_last_release +{ + note( 'Entering tests_check_last_release()' ) ; + + diag( check_last_release( 1.1 ) ) ; + # \Q \E here to avoid putting \ before each space + like( check_last_release( 1.1 ), qr/\Qis up to date\E/mxs, 'check_last_release: up to date' ) ; + like( check_last_release( 1.1 ), qr/1\.1/mxs, 'check_last_release: up to date, include number' ) ; + diag( check_last_release( 999.999 ) ) ; + like( check_last_release( 999.999 ), qr/\Qnot up to date\E/mxs, 'check_last_release: not up to date' ) ; + like( check_last_release( 999.999 ), qr/999\.999/mxs, 'check_last_release: not up to date, include number' ) ; + like( check_last_release( 'unknown' ), qr/\QImapsync public release is unknown\E/mxs, 'check_last_release: unknown' ) ; + like( check_last_release( 'timeout' ), qr/\QImapsync public release is unknown (timeout)\E/mxs, 'check_last_release: timeout' ) ; + like( check_last_release( 'lalala' ), qr/\QImapsync public release is unknown (lalala)\E/mxs, 'check_last_release: lalala' ) ; + diag( check_last_release( ) ) ; + + note( 'Leaving tests_check_last_release()' ) ; + return ; +} + +sub tests_imapsync_context +{ + note( 'Entering tests_imapsync_context()' ) ; + + like( imapsync_context( ), qr/^CGI|^Docker|^DockerCGI|^Standard/, 'imapsync_context: CGI or Docker or DockerCGI or Standard' ) ; + note( 'Leaving tests_imapsync_context()' ) ; + return ; +} + +sub imapsync_context +{ + my $mysync = shift ; + + my $context = q{} ; + + if ( under_docker_context( $mysync ) && under_cgi_context( $mysync ) ) + { + $context = 'DockerCGI' ; + } + elsif ( under_docker_context( $mysync ) ) + { + $context = 'Docker' ; + } + elsif ( under_cgi_context( $mysync ) ) + { + $context = 'CGI' ; + } + else + { + $context = 'Standard' ; + } + + return $context ; + +} + +sub imapsync_version +{ + my $mysync = shift ; + my $rcs = $mysync->{rcs} ; + my $version ; + + $version = version_from_rcs( $rcs ) ; + return( $version ) ; +} + + +sub tests_version_from_rcs +{ + note( 'Entering tests_version_from_rcs()' ) ; + + is( undef, version_from_rcs( ), 'version_from_rcs: no args => undef' ) ; + is( 1.831, version_from_rcs( q{imapsync,v 1.831 2017/08/27} ), 'version_from_rcs: imapsync,v 1.831 2017/08/27 => 1.831' ) ; + is( 'UNKNOWN', version_from_rcs( 1.831 ), 'version_from_rcs: 1.831 => UNKNOWN' ) ; + + note( 'Leaving tests_version_from_rcs()' ) ; + return ; +} + + +sub version_from_rcs +{ + + my $rcs = shift ; + if ( ! $rcs ) { return ; } + + my $version = 'UNKNOWN' ; + + if ( $rcs =~ m{,v\s+(\d+\.\d+)}mxso ) { + $version = $1 + } + + return( $version ) ; +} + + +sub tests_imapsync_basename +{ + note( 'Entering tests_imapsync_basename()' ) ; + + ok( imapsync_basename() =~ m/imapsync/, 'imapsync_basename: match imapsync'); + ok( 'blabla' ne imapsync_basename(), 'imapsync_basename: do not equal blabla'); + + note( 'Leaving tests_imapsync_basename()' ) ; + return ; +} + +sub imapsync_basename +{ + + return basename( $PROGRAM_NAME ) ; + +} + + +sub localhost_info +{ + my $mysync = shift ; + my( $infos ) = join( q{}, + "Here is imapsync ", imapsync_version( $mysync ), + " on host " . hostname(), + ", a $OSNAME system with ", + ram_memory_info( ), + "\n", + 'with Perl ', + mysprintf( '%vd ', $PERL_VERSION), + "and Mail::IMAPClient $Mail::IMAPClient::VERSION", + ) ; + return( $infos ) ; +} + +sub tests_cpu_number +{ + note( 'Entering tests_cpu_number()' ) ; + + is( 1, is_integer( cpu_number( ) ), "cpu_number: is_integer" ) ; + ok( 1 <= cpu_number( ), "cpu_number: 1 or more" ) ; + is( 1, cpu_number( 1 ), "cpu_number: 1 => 1" ) ; + is( 1, cpu_number( $MINUS_ONE ), "cpu_number: -1 => 1" ) ; + is( 1, cpu_number( 'lalala' ), "cpu_number: lalala => 1" ) ; + is( $NUMBER_42, cpu_number( $NUMBER_42 ), "cpu_number: $NUMBER_42 => $NUMBER_42" ) ; + + note( "cpu_number = " . cpu_number( ) . "\n" ) ; + note( "hostname = " . hostname( ) . "\n" ) ; + SKIP: { + if ( ! ( 'i005' eq hostname() ) ) + { + skip( 'cpu_number on host != i005 (FreeBSD)', 1 ) ; + } + is( 4, cpu_number( ), "cpu_number: on i005 (FreeBSD) => 4" ) ; + } ; + + SKIP: { + if ( ! ( 'petite' eq hostname() ) ) + { + skip( 'cpu_number on host != petite (Linux)', 1 ) ; + } + is( 2, cpu_number( ), "cpu_number: on petite (Linux) => 2" ) ; + } ; + + SKIP: { + if ( ! ( skip_macosx( ) ) ) + { + skip( 'cpu_number on host != polarhome macosx (Darwin MacOS X 10.7.5 Lion)', 1 ) ; + } + is( 2, cpu_number( ), "cpu_number: on polarhome macosx (Darwin MacOS X 10.7.5 Lion) => 2" ) ; + } ; + + SKIP: { + if ( ! ( 'pcHPDV7-HP' eq hostname() ) ) + { + skip( 'cpu_number on host != pcHPDV7-HP (Windows 7, 64bits)', 1 ) ; + } + is( 2, cpu_number( ), "cpu_number: on pcHPDV7-HP (Windows 7, 64bits) => 2" ) ; + } ; + + SKIP: { + if ( ! ( 'CUILLERE' eq hostname() ) ) + { + skip( 'cpu_number on host != CUILLERE (Windows XP, 32bits)', 1 ) ; + } + is( 1, cpu_number( ), "cpu_number: on CUILLERE (Windows XP, 32bits) => 1" ) ; + } ; + + + note( 'Leaving tests_cpu_number()' ) ; + return ; +} + + +sub cpu_number { + + my $cpu_number_forced = shift ; + # Well, here 1 is better than 0 or undef + my $cpu_number = 1 ; # Default value, erased if better found + + my @cpuinfo ; + if ( $ENV{"NUMBER_OF_PROCESSORS"} ) + { + # might be under a Windows system + $cpu_number = $ENV{"NUMBER_OF_PROCESSORS"} ; + #myprint( "Number of processors found by env var NUMBER_OF_PROCESSORS: $cpu_number\n" ) ; + } + + if ( 'darwin' eq $OSNAME ) + { + $cpu_number = backtick( "sysctl -n hw.ncpu" ) ; + chomp( $cpu_number ) ; + #myprint( "Number of processors found by cmd 'sysctl -n hw.ncpu': $cpu_number\n" ) ; + } + + if ( 'freebsd' eq $OSNAME ) + { + $cpu_number = backtick( "sysctl -n kern.smp.cpus" ) ; + chomp( $cpu_number ) ; + #myprint( "Number of processors found by cmd 'sysctl -n kern.smp.cpus': $cpu_number\n" ) ; + } + + if ( 'linux' eq $OSNAME && -e '/proc/cpuinfo' ) + { + @cpuinfo = file_to_array( '/proc/cpuinfo' ) ; + $cpu_number = grep { /^processor/mxs } @cpuinfo ; + #myprint( "Number of processors found via /proc/cpuinfo: $cpu_number\n" ) ; + } + + if ( defined $cpu_number_forced ) + { + $cpu_number = $cpu_number_forced ; + } + + return( integer_or_1( $cpu_number ) ) ; +} + +sub tests_integer_or_1 +{ + note( 'Entering tests_integer_or_1()' ) ; + + is( 1, integer_or_1( ), 'integer_or_1: no args => 1' ) ; + is( 1, integer_or_1( undef ), 'integer_or_1: undef => 1' ) ; + is( $NUMBER_10, integer_or_1( $NUMBER_10 ), 'integer_or_1: 10 => 10' ) ; + is( 1, integer_or_1( q{} ), 'integer_or_1: empty string => 1' ) ; + is( 1, integer_or_1( 'lalala' ), 'integer_or_1: lalala => 1' ) ; + + note( 'Leaving tests_integer_or_1()' ) ; + return ; +} + +sub integer_or_1 +{ + my $number = shift ; + if ( is_integer( $number ) ) { + return $number ; + } + # else + return 1 ; +} + +sub tests_is_integer +{ + note( 'Entering tests_is_integer()' ) ; + + is( undef, is_integer( ), 'is_integer: no args => undef ' ) ; + ok( is_integer( 1 ), 'is_integer: 1 => yes ') ; + ok( is_integer( $NUMBER_42 ), 'is_integer: 42 => yes ') ; + ok( is_integer( "$NUMBER_42" ), 'is_integer: "$NUMBER_42" => yes ') ; + ok( is_integer( '42' ), 'is_integer: "42" => yes ') ; + ok( is_integer( $NUMBER_104_857_600 ), 'is_integer: 104_857_600 => yes') ; + ok( is_integer( "$NUMBER_104_857_600" ), 'is_integer: "$NUMBER_104_857_600" => yes') ; + ok( is_integer( '104857600' ), 'is_integer: 104857600 => yes') ; + ok( ! is_integer( 'blabla' ), 'is_integer: blabla => no' ) ; + ok( ! is_integer( q{} ), 'is_integer: empty string => no' ) ; + + note( 'Leaving tests_is_integer()' ) ; + return ; +} + +sub is_integer +{ + my $number = shift ; + if ( ! defined $number ) { return ; } + return( $number =~ m{^\d+$}xo ) ; +} + + + + +sub tests_loadavg +{ + note( 'Entering tests_loadavg()' ) ; + + SKIP: { + skip( 'Tests for darwin', 3 ) if ('darwin' ne $OSNAME) ; + is( undef, loadavg( '/noexist' ), 'loadavg: /noexist => undef' ) ; + is_deeply( + [ '0.11', '0.22', '0.33' ], + [ loadavg( 'vm.loadavg: { 0.11 0.22 0.33 }' ) ], + 'loadavg: "vm.loadavg: { 0.11 0.22 0.33 }" => 0.11 0.22 0.33' + ) ; + note( join( " ", "loadavg:", loadavg( ) ) ) ; + is( 3, scalar( my @loadavg = loadavg( ) ), 'loadavg: 3 values' ) ; + } ; + + SKIP: { + skip( 'Tests for linux', 3 ) if ('linux' ne $OSNAME) ; + is( undef, loadavg( '/noexist' ), 'loadavg: /noexist => undef' ) ; + ok( loadavg( ), 'loadavg: no args' ) ; + + is_deeply( [ '0.39', '0.30', '0.37', '1/602' ], + [ loadavg( '0.39 0.30 0.37 1/602 6073' ) ], + 'loadavg 0.39 0.30 0.37 1/602 6073 => [0.39, 0.30, 0.37, 1/602]' ) ; + } ; + + SKIP: { + skip( 'Tests for Windows', 1 ) if ('MSWin32' ne $OSNAME) ; + is_deeply( [ 0 ], + [ loadavg( ) ], + 'loadavg on MSWin32 => 0' ) ; + } ; + + note( 'Leaving tests_loadavg()' ) ; + return ; +} + + +sub loadavg +{ + if ( 'linux' eq $OSNAME ) { + return ( loadavg_linux( @ARG ) ) ; + } + if ( 'freebsd' eq $OSNAME ) { + return ( loadavg_freebsd( @ARG ) ) ; + } + if ( 'darwin' eq $OSNAME ) { + return ( loadavg_darwin( @ARG ) ) ; + } + if ( 'MSWin32' eq $OSNAME ) { + return ( loadavg_windows( @ARG ) ) ; + } + return( 'unknown' ) ; +} + +sub loadavg_linux +{ + my $line = shift ; + + if ( ! $line ) { + $line = firstline( '/proc/loadavg' ) or return ; + } + + my ( $avg_1_min, $avg_5_min, $avg_15_min, $current_runs ) = split /\s/mxs, $line ; + if ( all_defined( $avg_1_min, $avg_5_min, $avg_15_min ) ) { + $sync->{ debug } and myprint( "System load: $avg_1_min $avg_5_min $avg_15_min $current_runs\n" ) ; + return ( $avg_1_min, $avg_5_min, $avg_15_min, $current_runs ) ; + } + return ; +} + +sub loadavg_freebsd +{ + my $file = shift ; + # Example of output of command "sysctl vm.loadavg": + # vm.loadavg: { 0.15 0.08 0.08 } + my $loadavg ; + + if ( ! defined $file ) { + eval { + $loadavg = `/sbin/sysctl vm.loadavg` ; + #myprint( "LOADAVG FREEBSD: $loadavg\n" ) ; + } ; + if ( $EVAL_ERROR ) { myprint( "[$EVAL_ERROR]\n" ) ; return ; } + }else{ + $loadavg = firstline( $file ) or return ; + } + + my ( $avg_1_min, $avg_5_min, $avg_15_min ) + = $loadavg =~ /vm\.loadavg\s*[:=]\s*\{?\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+(\d+\.?\d*)/mxs ; + $sync->{ debug } and myprint( "System load: $avg_1_min $avg_5_min $avg_15_min\n" ) ; + return ( $avg_1_min, $avg_5_min, $avg_15_min ) ; +} + +sub loadavg_darwin +{ + my $line = shift ; + # Example of output of command "sysctl vm.loadavg": + # vm.loadavg: { 0.15 0.08 0.08 } + my $loadavg ; + + if ( ! defined $line ) { + eval { + # $loadavg = `/usr/sbin/sysctl vm.loadavg` ; + $loadavg = `LANG= /usr/sbin/sysctl vm.loadavg` ; + #myprint( "LOADAVG DARWIN: $loadavg\n" ) ; + } ; + if ( $EVAL_ERROR ) { myprint( "[$EVAL_ERROR]\n" ) ; return ; } + }else{ + $loadavg = $line ; + } + + my ( $avg_1_min, $avg_5_min, $avg_15_min ) + = $loadavg =~ /vm\.loadavg\s*[:=]\s*\{?\s*(\d+\.?\d*)\s+(\d+\.?\d*)\s+(\d+\.?\d*)/mxs ; + #$sync->{ debug } and myprint( "System load: $avg_1_min $avg_5_min $avg_15_min\n" ) ; + return ( $avg_1_min, $avg_5_min, $avg_15_min ) ; +} + +sub loadavg_windows +{ + my $file = shift ; + # Example of output of command "wmic cpu get loadpercentage": + # LoadPercentage + # 12 + my $loadavg ; + + if ( ! defined $file ) { + eval { + #$loadavg = `CMD wmic cpu get loadpercentage` ; + $loadavg = "LoadPercentage\n0\n" ; + #myprint( "LOADAVG WIN: $loadavg\n" ) ; + } ; + if ( $EVAL_ERROR ) { myprint( "[$EVAL_ERROR]\n" ) ; return ; } + }else{ + $loadavg = file_to_string( $file ) or return ; + #myprint( "$loadavg" ) ; + } + $loadavg =~ /LoadPercentage\n(\d+)/xms ; + my $num = $1 ; + $num /= 100 ; + + $sync->{ debug } and myprint( "System load: $num\n" ) ; + return ( $num ) ; +} + + + + + + +sub tests_load_and_delay +{ + note( 'Entering tests_load_and_delay()' ) ; + + is( undef, load_and_delay( ), 'load_and_delay: no args => undef ' ) ; + is( undef, load_and_delay( 1 ), 'load_and_delay: not 4 args => undef ' ) ; + is( undef, load_and_delay( 0, 1, 1, 1 ), 'load_and_delay: division per 0 => undef ' ) ; + +# ( $cpu_num, $avg_1_min, $avg_5_min, $avg_15_min ) + + is( 0, load_and_delay( 1, 1, 1, 1 ), 'load_and_delay: one core, loads are all 1 => ok ' ) ; + is( 0, load_and_delay( 1, 1, 1, 1, 'lalala' ), 'load_and_delay: five arguments is ok' ) ; + is( 0, load_and_delay( 2, 2, 2, 2 ), 'load_and_delay: two core, loads are all 2 => ok ' ) ; + is( 0, load_and_delay( 2, 2, 4, 5 ), 'load_and_delay: two core, load1m is 2 => ok ' ) ; + + + is( 0, load_and_delay( 1, 0, 0, 0 ), 'load_and_delay: one core, load1m=0 load5m=0 load15m=0 => 0 ' ) ; + is( 0, load_and_delay( 1, 0, 0, 2 ), 'load_and_delay: one core, load1m=0 load5m=0 load15m=2 => 0 ' ) ; + is( 0, load_and_delay( 1, 0, 2, 0 ), 'load_and_delay: one core, load1m=0 load5m=2 load15m=0 => 0 ' ) ; + is( 0, load_and_delay( 1, 0, 2, 2 ), 'load_and_delay: one core, load1m=0 load5m=2 load15m=2 => 0 ' ) ; + is( 0, load_and_delay( 1, 0, 3, 3 ), 'load_and_delay: one core, load1m=0 load5m=3 load15m=3 => 0 ' ) ; + is( 0, load_and_delay( 1, 0, 4, 4 ), 'load_and_delay: one core, load1m=0 load5m=3 load15m=3 => 0 ' ) ; + is( 0, load_and_delay( 1, 2, 0, 0 ), 'load_and_delay: one core, load1m=2 load5m=0 load15m=0 => 0 ' ) ; + is( 0, load_and_delay( 1, 2, 0, 2 ), 'load_and_delay: one core, load1m=2 load5m=0 load15m=2 => 0 ' ) ; + is( 0, load_and_delay( 1, 2, 2, 0 ), 'load_and_delay: one core, load1m=2 load5m=2 load15m=0 => 0 ' ) ; + is( 0, load_and_delay( 1, 2, 2, 2 ), 'load_and_delay: one core, load1m=2 load5m=2 load15m=2 => 0 ' ) ; + is( 0, load_and_delay( 1, 2.9, 2.9, 2.9 ), 'load_and_delay: one core, load1m=2.9 load5m=2.9 load15m=2.9 => 0 ' ) ; + + is( 0, load_and_delay( 1, 3, 0, 0 ), 'load_and_delay: one core, load1m=3 load5m=0 load15m=0 => 0 ' ) ; + is( 0, load_and_delay( 1, 3, 2.9, 2.9 ), 'load_and_delay: one core, load1m=3 load5m=2.9 load15m=2.9 => 0 ' ) ; + is( 0, load_and_delay( 1, 3, 3, 2.9 ), 'load_and_delay: one core, load1m=3 load5m=3 load15m=2.9 => 0 ' ) ; + is( 0, load_and_delay( 1, 3, 3, 3 ), 'load_and_delay: one core, load1m=3 load5m=3 load15m=3 => 0 ' ) ; + + is( 1, load_and_delay( 1, 6, 0, 0 ), 'load_and_delay: one core, load1m=3 load5m=0 load15m=0 => 1 ' ) ; + is( 1, load_and_delay( 1, 6, 5.9, 5.9 ), 'load_and_delay: one core, load1m=3 load5m=2.9 load15m=2.9 => 1 ' ) ; + is( 5, load_and_delay( 1, 6, 6, 5.9 ), 'load_and_delay: one core, load1m=3 load5m=3 load15m=2.9 => 5 ' ) ; + is( 15, load_and_delay( 1, 6, 6, 6 ), 'load_and_delay: one core, load1m=3 load5m=3 load15m=3 => 15 ' ) ; + + + + note( 'Leaving tests_load_and_delay()' ) ; + return ; +} + +sub load_and_delay +{ + # Basically return 0 if load is not heavy, ie <= 1 per processor + + # Not enough arguments + if ( 4 > scalar @ARG ) { return ; } + + my ( $cpu_num, $avg_1_min, $avg_5_min, $avg_15_min ) = @ARG ; + + if ( 0 == $cpu_num ) { return ; } + + # Let divide by number of cores + ( $avg_1_min, $avg_5_min, $avg_15_min ) = map { $_ / $cpu_num } ( $avg_1_min, $avg_5_min, $avg_15_min ) ; + # One of avg ok => ok, for now it is a OR + if ( $avg_1_min < 6 ) { return 0 ; } + if ( $avg_5_min < 6 ) { return 1 ; } # Retry in 1 minute + if ( $avg_15_min < 6 ) { return 5 ; } # Retry in 5 minutes + return 15 ; # Retry in 15 minutes +} + + +sub tests_cpu_time +{ + note( 'Entering tests_cpu_time()' ) ; + + ok( is_number( cpu_time( ) ), 'cpu_time: no args => a number' ) ; + + my $mysync = { } ; + $mysync->{ debug } = 1 ; + ok( is_number( cpu_time( $mysync ) ), 'cpu_time: {} => a number' ) ; + + note( 'Leaving tests_cpu_time()' ) ; + return ; +} + +sub cpu_time +{ + my $mysync = shift ; + + my @cpu_times = times ; + if ( ! @cpu_times ) { return ; } + + my $cpu_time = 0 ; + # last element is the sum of all elements + $cpu_time = ( map { $cpu_time += $_ } @cpu_times )[ -1 ] ; + my $cpu_time_round = mysprintf( '%.2f', $cpu_time ) ; + $mysync->{ debug } and myprint( join(' + ', @cpu_times), " = $cpu_time ~ $cpu_time_round\n" ) ; + return $cpu_time ; +} + + +sub tests_cpu_percent +{ + note( 'Entering tests_cpu_percent()' ) ; + + is( '0.0', cpu_percent( ), 'cpu_percent: no args => 0.0' ) ; + my $mysync = { } ; + $mysync->{ debug } = 1 ; + is( '0.0', cpu_percent( $mysync ), 'cpu_percent: {} => 0.0' ) ; + is( '0.0', cpu_percent( $mysync, 0 ), 'cpu_percent: {} 0 => 0.0' ) ; + is( '300.0', cpu_percent( $mysync, 3 ), 'cpu_percent: {} 3 => 300.0' ) ; + is( '30.0', cpu_percent( $mysync, 3, 10 ), 'cpu_percent: {} 3 10 => 30.0' ) ; + is( '0.0', cpu_percent( $mysync, 0, 10 ), 'cpu_percent: {} 0 10 => 0.0' ) ; + + note( 'Leaving tests_cpu_percent()' ) ; + return ; +} + +sub cpu_percent +{ + my $mysync = shift ; + my $cpu_time = shift || 0 ; + my $timediff = shift || 1 ; # no division by 0 + + if ( $cpu_time > $timediff ) + { + myprint( "Strange: cpu_time $cpu_time > timediff $timediff\n" ) ; + } + my $cpu_percent = 0 ; + $cpu_percent = mysprintf( '%.1f', 100 * $cpu_time / $timediff ) ; + $mysync->{ debug } and myprint( "cpu_percent: $cpu_percent \n" ) ; + + return $cpu_percent ; + +} + +sub tests_cpu_percent_global +{ + note( 'Entering tests_cpu_percent_global()' ) ; + + is( '0.0', cpu_percent_global( ), 'cpu_percent_global: no args => 0' ) ; + my $mysync = { } ; + $mysync->{ debug } = 1 ; + is( '0.0', cpu_percent_global( $mysync ), 'cpu_percent_global: {} => 0' ) ; + is( '0.0', cpu_percent_global( $mysync, 0 ), 'cpu_percent_global: {} 0 => 0' ) ; + + SKIP: { + if ( ! ( 'i005' eq hostname() ) ) + { + skip( 'cpu_percent_global on host != i005', 1 ) ; + } + is( '25.0', cpu_percent_global( $mysync, 100 ), 'cpu_percent_global: {} 100 => 25 on host i005' ) ; + } ; + + SKIP: { + if ( ! ( 'petite' eq hostname() ) ) + { + skip( 'cpu_percent_global on host != petite', 1 ) ; + } + is( '50.0', cpu_percent_global( $mysync, 100 ), 'cpu_percent_global: {} 100 => 50 on host petite' ) ; + } ; + + note( 'Leaving tests_cpu_percent_global()' ) ; + return ; +} + +sub cpu_percent_global +{ + my $mysync = shift ; + my $cpu_percent = shift || 0 ; + + my $cpu_number = cpu_number( ) ; + + my $cpu_percent_global ; + $cpu_percent_global = mysprintf( '%.1f', $cpu_percent / $cpu_number ) ; + $mysync->{ debug } and myprint( "cpu_percent_global: $cpu_percent_global \n" ) ; + + return( $cpu_percent_global ) ; +} + + +sub ram_memory_info +{ + # In GigaBytes so division by 1024 * 1024 * 1024 + # + return( + sprintf( "%.1f/%.1f free GiB of RAM", + Sys::MemInfo::get("freemem") / ( $KIBI ** 3 ), + Sys::MemInfo::get("totalmem") / ( $KIBI ** 3 ), + ) + ) ; +} + + + +sub tests_memory_stress +{ + note( 'Entering tests_memory_stress()' ) ; + + is( undef, memory_stress( ), 'memory_stress: => undef' ) ; + + note( 'Leaving tests_memory_stress()' ) ; + return ; +} + +sub memory_stress +{ + my $total_ram_in_MB = Sys::MemInfo::get("totalmem") / ( $KIBI * $KIBI ) ; + my $i = 1 ; + + myprintf("Stress memory consumption before: %.1f MiB\n", memory_consumption( ) / $KIBI / $KIBI ) ; + while ( $i < $total_ram_in_MB / 1.7 ) { $a .= "A" x 1000_000; $i++ } ; + myprintf("Stress memory consumption after: %.1f MiB\n", memory_consumption( ) / $KIBI / $KIBI ) ; + return ; +} + +sub tests_memory_consumption +{ + note( 'Entering tests_memory_consumption()' ) ; + + note( "memory_consumption: " . memory_consumption() . " bytes aka " . bytes_display_string_dec( memory_consumption() ) ) ; + like( memory_consumption( ), qr{\d+}xms,'memory_consumption no args') ; + like( memory_consumption( 1 ), qr{\d+}xms,'memory_consumption 1') ; + like( memory_consumption( $PROCESS_ID ), qr{\d+}xms,"memory_consumption_of_pids $PROCESS_ID") ; + + like( memory_consumption_ratio(), qr{\d+}xms, 'memory_consumption_ratio' ) ; + like( memory_consumption_ratio(1), qr{\d+}xms, 'memory_consumption_ratio 1' ) ; + like( memory_consumption_ratio(10), qr{\d+}xms, 'memory_consumption_ratio 10' ) ; + + + note( 'Leaving tests_memory_consumption()' ) ; + return ; +} + +sub memory_consumption +{ + # memory consumed by imapsync until now in bytes + return( ( memory_consumption_of_pids( ) )[0] ); +} + +sub debugmemory +{ + my $mysync = shift ; + if ( ! $mysync->{debugmemory} ) { return q{} ; } + + my $precision = shift ; + return( mysprintf( "Memory consumption$precision: %.1f MiB\n", memory_consumption( ) / $KIBI / $KIBI ) ) ; +} + +sub memory_consumption_of_pids +{ + + my @pid = @_; + @pid = ( @pid ) ? @pid : ( $PROCESS_ID ) ; + + $sync->{ debug } and myprint( "memory_consumption_of_pids PIDs: @pid\n" ) ; + my @val ; + if ( ( 'MSWin32' eq $OSNAME ) or ( 'cygwin' eq $OSNAME ) ) { + @val = memory_consumption_of_pids_win32( @pid ) ; + } + elsif ( 'darwin' eq $OSNAME ) + { + @val = memory_consumption_of_pids_mac( @pid ) ; + } + else + { + # Unix + my @ps = qx{ ps -o vsz -p @pid } ; + shift @ps ; # First line is column name "VSZ" + chomp @ps ; + # convert to octets + + @val = map { $_ * $KIBI } @ps ; + } + return( @val ) ; +} + + +sub memory_consumption_of_pids_mac +{ + my @pid = @_ ; + # Use IPC::Open3 from perlcrit -3 + # But it stalls on Darwin, I don't understand why! + #my @ps = backtick( "ps -o rss -p @pid" ) ; + #myprint( "ps: @ps" ) ; + my @ps = qx{ ps -o rss -p @pid } ; + shift @ps ; # First line is column name "RSS" + chomp @ps ; + my @val = map { $_ * $KIBI } @ps ; + return( @val ) ; +} + +sub memory_consumption_of_pids_win32 +{ + # Windows + my @PID = @_; + my %PID; + # hash of pids as key values + map { $PID{$_}++ } @PID; + + # Does not work but should work reading the tasklist documentation + #@ps = qx{ tasklist /FI "PID eq @PID" }; + + my @ps = qx{ tasklist /NH /FO CSV } ; + #my @ps = backtick( 'tasklist /NH /FO CSV' ) ; + #myprint( "-" x $STD_CHAR_PER_LINE, "\n", @ps, "-" x $STD_CHAR_PER_LINE, "\n" ) ; + my @val; + foreach my $line (@ps) { + my($name, $pid, $mem) = (split ',', $line )[0,1,4]; + next if (! $pid); + #myprint( "[$name][$pid][$mem]" ) ; + if ($PID{remove_qq($pid)}) { + #myprint( "MATCH !\n" ) ; + chomp $mem ; + $mem = remove_qq($mem); + $mem = remove_Ko($mem); + $mem = remove_not_num($mem); + #myprint( "[$mem]\n" ) ; + push @val, $mem * $KIBI; + } + } + return(@val); +} + + +sub tests_backtick +{ + note( 'Entering tests_backtick()' ) ; + + is( undef, backtick( ), 'backtick: no args' ) ; + is( undef, backtick( q{} ), 'backtick: empty command' ) ; + + SKIP: { + skip( 'test for MSWin32', 5 ) if ('MSWin32' ne $OSNAME) ; + my @output ; + @output = backtick( 'echo Hello World!' ) ; + # Add \r on Windows. + ok( "Hello World!\r\n" eq $output[0], 'backtick: echo Hello World!' ) ; + $sync->{ debug } and myprint( "[@output]" ) ; + @output = backtick( 'echo Hello & echo World!' ) ; + ok( "Hello \r\n" eq $output[0], 'backtick: echo Hello & echo World! line 1' ) ; + ok( "World!\r\n" eq $output[1], 'backtick: echo Hello & echo World! line 2' ) ; + $sync->{ debug } and myprint( "[@output][$output[0]][$output[1]]" ) ; + # Scalar context + ok( "Hello World!\r\n" eq backtick( 'echo Hello World!' ), + 'backtick: echo Hello World! scalar' ) ; + ok( "Hello \r\nWorld!\r\n" eq backtick( 'echo Hello & echo World!' ), + 'backtick: echo Hello & echo World! scalar 2 lines' ) ; + } ; + SKIP: { + skip( 'test for Unix', 7 ) if ('MSWin32' eq $OSNAME) ; + is( undef, backtick( 'aaaarrrg' ), 'backtick: aaaarrrg command not found' ) ; + # Array context + my @output ; + @output = backtick( 'echo Hello World!' ) ; + ok( "Hello World!\n" eq $output[0], 'backtick: echo Hello World!' ) ; + $sync->{ debug } and myprint( "[@output]" ) ; + @output = backtick( "echo Hello\necho World!" ) ; + ok( "Hello\n" eq $output[0], 'backtick: echo Hello; echo World! line 1' ) ; + ok( "World!\n" eq $output[1], 'backtick: echo Hello; echo World! line 2' ) ; + $sync->{ debug } and myprint( "[@output]" ) ; + # Scalar context + ok( "Hello World!\n" eq backtick( 'echo Hello World!' ), + 'backtick: echo Hello World! scalar' ) ; + ok( "Hello\nWorld!\n" eq backtick( "echo Hello\necho World!" ), + 'backtick: echo Hello; echo World! scalar 2 lines' ) ; + # Return error positive value, that's ok + is( undef, backtick( 'false' ), 'backtick: false returns no output' ) ; + my $mem = backtick( "ps -o vsz -p $PROCESS_ID" ) ; + $sync->{ debug } and myprint( "MEM=$mem\n" ) ; + + } + + note( 'Leaving tests_backtick()' ) ; + return ; +} + + +sub backtick +{ + my $command = shift ; + + if ( ! $command ) { return ; } + + my ( $writer, $reader, $err ) ; + my @output ; + my $pid ; + my $eval = eval { + $pid = IPC::Open3::open3( $writer, $reader, $err, $command ) ; + } ; + if ( $EVAL_ERROR ) { + myprint( $EVAL_ERROR ) ; + return ; + } + if ( ! $eval ) { return ; } + if ( ! $pid ) { return ; } + waitpid( $pid, 0 ) ; + @output = <$reader>; # Output here + # + #my @errors = <$err>; #Errors here, instead of the console + if ( not @output ) { return ; } + #myprint( @output ) ; + + if ( $output[0] =~ /\Qopen3: exec of $command failed\E/mxs ) { return ; } + if ( wantarray ) { + return( @output ) ; + } else { + return( join( q{}, @output) ) ; + } +} + + + +sub tests_check_binary_embed_all_dyn_libs +{ + note( 'Entering tests_check_binary_embed_all_dyn_libs()' ) ; + + is( 1, check_binary_embed_all_dyn_libs( ), 'check_binary_embed_all_dyn_libs: no args => 1' ) ; + + note( 'Leaving tests_check_binary_embed_all_dyn_libs()' ) ; + + return ; +} + + +sub check_binary_embed_all_dyn_libs +{ + my @search_dyn_lib_locale = search_dyn_lib_locale( ) ; + + if ( @search_dyn_lib_locale ) + { + myprint( "Found myself $PROGRAM_NAME pid $PROCESS_ID using locale dynamic libraries that seems out of myself:\n" ) ; + myprint( @search_dyn_lib_locale ) ; + if ( $PROGRAM_NAME =~ m{imapsync_bin_Darwin} ) + { + return 0 ; + } + elsif ( $PROGRAM_NAME =~ m{imapsync.*\.exe} ) + { + return 0 ; + } + else + { + # is always ok for non binary + return 1 ; + } + } + else + { + # Found only embedded dynamic lib + myprint( "Found only embedded dynamic lib. Good!\n" ) ; + return 1 ; + } +} + +sub search_dyn_lib_locale +{ + if ( 'darwin' eq $OSNAME ) + { + return search_dyn_lib_locale_darwin( ) ; + } + if ( 'linux' eq $OSNAME ) + { + return search_dyn_lib_locale_linux( ) ; + } + if ( 'MSWin32' eq $OSNAME ) + { + return search_dyn_lib_locale_MSWin32( ) ; + } + +} + +sub search_dyn_lib_locale_darwin +{ + my $command = qq{ lsof -p $PROCESS_ID | grep ' REG ' | grep .dylib | grep -v '/par-' } ; + myprint( "Search non embeded dynamic libs with the command: $command\n" ) ; + return backtick( $command ) ; +} + +sub search_dyn_lib_locale_linux +{ + my $command = qq{ lsof -p $PROCESS_ID | grep ' REG ' | grep -v '/tmp/par-' | grep '\.so' } ; + myprint( "Search non embeded dynamic libs with the command: $command\n" ) ; + return backtick( $command ) ; +} + +sub search_dyn_lib_locale_MSWin32 +{ + my $command = qq{ Listdlls.exe $PROCESS_ID|findstr Strawberry } ; + # $command = qq{ Listdlls.exe $PROCESS_ID|findstr Strawberry } ; + myprint( "Search non embeded dynamic libs with the command: $command\n" ) ; + return qx( $command ) ; +} + + + +sub remove_not_num +{ + + my $string = shift ; + $string =~ tr/0-9//cd ; + #myprint( "tr [$string]\n" ) ; + return( $string ) ; +} + +sub tests_remove_not_num +{ + note( 'Entering tests_remove_not_num()' ) ; + + ok( '123' eq remove_not_num( 123 ), 'remove_not_num( 123 )' ) ; + ok( '123' eq remove_not_num( '123' ), q{remove_not_num( '123' )} ) ; + ok( '123' eq remove_not_num( '12 3' ), q{remove_not_num( '12 3' )} ) ; + ok( '123' eq remove_not_num( 'a 12 3 Ko' ), q{remove_not_num( 'a 12 3 Ko' )} ) ; + + note( 'Leaving tests_remove_not_num()' ) ; + return ; +} + +sub remove_Ko +{ + my $string = shift; + if ($string =~ /^(.*)\sKo$/xo) { + return($1); + }else{ + return($string); + } +} + +sub remove_qq +{ + my $string = shift; + if ($string =~ /^"(.*)"$/xo) { + return($1); + }else{ + return($string); + } +} + +sub memory_consumption_ratio +{ + + my ($base) = @_; + $base ||= 1; + my $consu = memory_consumption(); + return($consu / $base); +} + + +sub date_from_rcs +{ + my $d = shift ; + + my %num2mon = qw( 01 Jan 02 Feb 03 Mar 04 Apr 05 May 06 Jun 07 Jul 08 Aug 09 Sep 10 Oct 11 Nov 12 Dec ) ; + if ($d =~ m{(\d{4})/(\d{2})/(\d{2})\s(\d{2}):(\d{2}):(\d{2})}xo ) { + # Handles the following format + # 2015/07/10 11:05:59 -- Generated by RCS Date tag. + #myprint( "$d\n" ) ; + #myprint( "header: [$1][$2][$3][$4][$5][$6]\n" ) ; + my ($year, $month, $day, $hour, $min, $sec) = ($1,$2,$3,$4,$5,$6) ; + $month = $num2mon{$month} ; + $d = "$day-$month-$year $hour:$min:$sec +0000" ; + #myprint( "$d\n" ) ; + } + return( $d ) ; +} + +sub tests_date_from_rcs +{ + note( 'Entering tests_date_from_rcs()' ) ; + + ok('19-Sep-2015 16:11:07 +0000' + eq date_from_rcs('Date: 2015/09/19 16:11:07 '), 'date_from_rcs from RCS date' ) ; + + note( 'Leaving tests_date_from_rcs()' ) ; + return ; +} + +sub good_date +{ + # two incoming formats: + # header Tue, 24 Aug 2010 16:00:00 +0200 + # internal 24-Aug-2010 16:00:00 +0200 + + # outgoing format: internal date format + # 24-Aug-2010 16:00:00 +0200 + + my $d = shift ; + return(q{}) if not defined $d; + + SWITCH: { + if ( $d =~ m{(\d?)(\d-...-\d{4})(\s\d{2}:\d{2}:\d{2})(\s(?:\+|-)\d{4})?}xo ) { + #myprint( "internal: [$1][$2][$3][$4]\n" ) ; + my ($day_1, $date_rest, $hour, $zone) = ($1,$2,$3,$4) ; + $day_1 = '0' if ($day_1 eq q{}) ; + $zone = ' +0000' if not defined $zone ; + $d = $day_1 . $date_rest . $hour . $zone ; + last SWITCH ; + } + + if ($d =~ m{(?:\w{3,},\s)?(\d{1,2}),?\s+(\w{3,})\s+(\d{2,4})\s+(\d{1,2})(?::|\.)(\d{1,2})(?:(?::|\.)(\d{1,2}))?\s*((?:\+|-)\d{4})?}xo ) { + # Handles any combination of following formats + # Tue, 24 Aug 2010 16:00:00 +0200 -- Standard + # 24 Aug 2010 16:00:00 +0200 -- Missing Day of Week + # Tue, 24 Aug 97 16:00:00 +0200 -- Two digit year + # Tue, 24 Aug 1997 16.00.00 +0200 -- Periods instead of colons + # Tue, 24 Aug 1997 16:00:00 +0200 -- Extra whitespace between year and hour + # Tue, 24 Aug 1997 6:5:2 +0200 -- Single digit hour, min, or second + # Tue, 24, Aug 1997 16:00:00 +0200 -- Extra comma + + #myprint( "header: [$1][$2][$3][$4][$5][$6][$7][$8]\n" ) ; + my ($day, $month, $year, $hour, $min, $sec, $zone) = ($1,$2,$3,$4,$5,$6,$7,$8); + $year = '19' . $year if length($year) == 2 && $year =~ m/^[789]/xo; + $year = '20' . $year if length($year) == 2; + + $month = substr $month, 0, 3 if length($month) > 4; + $day = mysprintf( '%02d', $day); + $hour = mysprintf( '%02d', $hour); + $min = mysprintf( '%02d', $min); + $sec = '00' if not defined $sec ; + $sec = mysprintf( '%02d', $sec ) ; + $zone = '+0000' if not defined $zone ; + $d = "$day-$month-$year $hour:$min:$sec $zone" ; + last SWITCH ; + } + + if ($d =~ m{(?:.{3})\s(...)\s+(\d{1,2})\s(\d{1,2}):(\d{1,2}):(\d{1,2})\s(?:\w{3})?\s?(\d{4})}xo ) { + # Handles any combination of following formats + # Sun Aug 20 11:55:09 2006 + # Wed Jan 24 11:58:38 MST 2007 + # Wed Jan 2 08:40:57 2008 + + #myprint( "header: [$1][$2][$3][$4][$5][$6]\n" ) ; + my ($month, $day, $hour, $min, $sec, $year) = ($1,$2,$3,$4,$5,$6); + $day = mysprintf( '%02d', $day ) ; + $hour = mysprintf( '%02d', $hour ) ; + $min = mysprintf( '%02d', $min ) ; + $sec = mysprintf( '%02d', $sec ) ; + $d = "$day-$month-$year $hour:$min:$sec +0000" ; + last SWITCH ; + } + my %num2mon = qw( 01 Jan 02 Feb 03 Mar 04 Apr 05 May 06 Jun 07 Jul 08 Aug 09 Sep 10 Oct 11 Nov 12 Dec ) ; + + if ($d =~ m{(\d{4})/(\d{2})/(\d{2})\s(\d{2}):(\d{2}):(\d{2})}xo ) { + # Handles the following format + # 2015/07/10 11:05:59 -- Generated by RCS Date tag. + #myprint( "$d\n" ) ; + #myprint( "header: [$1][$2][$3][$4][$5][$6]\n" ) ; + my ($year, $month, $day, $hour, $min, $sec) = ($1,$2,$3,$4,$5,$6) ; + $month = $num2mon{$month} ; + $d = "$day-$month-$year $hour:$min:$sec +0000" ; + #myprint( "$d\n" ) ; + last SWITCH ; + } + + if ($d =~ m{(\d{2})/(\d{2})/(\d{2})\s(\d{2}):(\d{2}):(\d{2})}xo ) { + # Handles the following format + # 02/06/09 22:18:08 -- Generated by AVTECH TemPageR devices + + #myprint( "header: [$1][$2][$3][$4][$5][$6]\n" ) ; + my ($month, $day, $year, $hour, $min, $sec) = ($1,$2,$3,$4,$5,$6); + $year = '20' . $year; + $month = $num2mon{$month}; + $d = "$day-$month-$year $hour:$min:$sec +0000"; + last SWITCH ; + } + + if ($d =~ m{\w{6,},\s(\w{3})\w+\s+(\d{1,2}),\s(\d{4})\s(\d{2}):(\d{2})\s(AM|PM)}xo ) { + # Handles the following format + # Saturday, December 14, 2002 05:00 PM - KBtoys.com order confirmations + + my ($month, $day, $year, $hour, $min, $apm) = ($1,$2,$3,$4,$5,$6); + + $hour += 12 if $apm eq 'PM' ; + $day = mysprintf( '%02d', $day ) ; + $d = "$day-$month-$year $hour:$min:00 +0000" ; + last SWITCH ; + } + + if ($d =~ m{(\w{3})\s(\d{1,2})\s(\d{4})\s(\d{2}):(\d{2}):(\d{2})\s((?:\+|-)\d{4})}xo ) { + # Handles the following format + # Saturday, December 14, 2002 05:00 PM - jr.com order confirmations + + my ($month, $day, $year, $hour, $min, $sec, $zone) = ($1,$2,$3,$4,$5,$6,$7); + + $day = mysprintf( '%02d', $day ) ; + $d = "$day-$month-$year $hour:$min:$sec $zone"; + last SWITCH ; + } + + if ($d =~ m{(\d{1,2})-(\w{3})-(\d{4})}xo ) { + # Handles the following format + # 21-Jun-2001 - register.com domain transfer email circa 2001 + + my ($day, $month, $year) = ($1,$2,$3); + $day = mysprintf( '%02d', $day); + $d = "$day-$month-$year 11:11:11 +0000"; + last SWITCH ; + } + + # unknown or unmatch => return same string + return($d); + } + + $d = qq("$d") ; + return( $d ) ; +} + + +sub tests_good_date +{ + note( 'Entering tests_good_date()' ) ; + + ok(q{} eq good_date(), 'good_date no arg'); + ok('"24-Aug-2010 16:00:00 +0200"' eq good_date('24-Aug-2010 16:00:00 +0200'), 'good_date internal 2digit zone'); + ok('"24-Aug-2010 16:00:00 +0000"' eq good_date('24-Aug-2010 16:00:00'), 'good_date internal 2digit no zone'); + ok('"01-Sep-2010 16:00:00 +0200"' eq good_date( '1-Sep-2010 16:00:00 +0200'), 'good_date internal SP 1digit'); + ok('"24-Aug-2010 16:00:00 +0200"' eq good_date('Tue, 24 Aug 2010 16:00:00 +0200'), 'good_date header 2digit zone'); + ok('"01-Sep-2010 16:00:00 +0000"' eq good_date('Wed, 1 Sep 2010 16:00:00'), 'good_date header SP 1digit zone'); + ok('"01-Sep-2010 16:00:00 +0200"' eq good_date('Wed, 1 Sep 2010 16:00:00 +0200'), 'good_date header SP 1digit zone'); + ok('"01-Sep-2010 16:00:00 +0200"' eq good_date('Wed, 1 Sep 2010 16:00:00 +0200 (CEST)'), 'good_date header SP 1digit zone'); + ok('"06-Feb-2009 22:18:08 +0000"' eq good_date('02/06/09 22:18:08'), 'good_date header TemPageR'); + ok('"02-Jan-2008 08:40:57 +0000"' eq good_date('Wed Jan 2 08:40:57 2008'), 'good_date header dice.com support 1digit day'); + ok('"20-Aug-2006 11:55:09 +0000"' eq good_date('Sun Aug 20 11:55:09 2006'), 'good_date header dice.com support 2digit day'); + ok('"24-Jan-2007 11:58:38 +0000"' eq good_date('Wed Jan 24 11:58:38 MST 2007'), 'good_date header status-now.com'); + ok('"24-Aug-2010 16:00:00 +0200"' eq good_date('24 Aug 2010 16:00:00 +0200'), 'good_date header missing date of week'); + ok('"24-Aug-2067 16:00:00 +0200"' eq good_date('Tue, 24 Aug 67 16:00:00 +0200'), 'good_date header 2digit year'); + ok('"24-Aug-1977 16:00:00 +0200"' eq good_date('Tue, 24 Aug 77 16:00:00 +0200'), 'good_date header 2digit year'); + ok('"24-Aug-1987 16:00:00 +0200"' eq good_date('Tue, 24 Aug 87 16:00:00 +0200'), 'good_date header 2digit year'); + ok('"24-Aug-1997 16:00:00 +0200"' eq good_date('Tue, 24 Aug 97 16:00:00 +0200'), 'good_date header 2digit year'); + ok('"24-Aug-2004 16:00:00 +0200"' eq good_date('Tue, 24 Aug 04 16:00:00 +0200'), 'good_date header 2digit year'); + ok('"24-Aug-1997 16:00:00 +0200"' eq good_date('Tue, 24 Aug 1997 16.00.00 +0200'), 'good_date header period time sep'); + ok('"24-Aug-1997 16:00:00 +0200"' eq good_date('Tue, 24 Aug 1997 16:00:00 +0200'), 'good_date header extra white space type1'); + ok('"24-Aug-1997 05:06:02 +0200"' eq good_date('Tue, 24 Aug 1997 5:6:2 +0200'), 'good_date header 1digit time vals'); + ok('"24-Aug-1997 05:06:02 +0200"' eq good_date('Tue, 24, Aug 1997 05:06:02 +0200'), 'good_date header extra commas'); + ok('"01-Oct-2003 12:45:24 +0000"' eq good_date('Wednesday, 01 October 2003 12:45:24 CDT'), 'good_date header no abbrev'); + ok('"11-Jan-2005 17:58:27 -0500"' eq good_date('Tue, 11 Jan 2005 17:58:27 -0500'), 'good_date extra white space'); + ok('"18-Dec-2002 15:07:00 +0000"' eq good_date('Wednesday, December 18, 2002 03:07 PM'), 'good_date kbtoys.com orders'); + ok('"16-Dec-2004 02:01:49 -0500"' eq good_date('Dec 16 2004 02:01:49 -0500'), 'good_date jr.com orders'); + ok('"21-Jun-2001 11:11:11 +0000"' eq good_date('21-Jun-2001'), 'good_date register.com domain transfer'); + ok('"18-Nov-2012 18:34:38 +0100"' eq good_date('Sun, 18 Nov 2012 18:34:38 +0100'), 'good_date pop2imap bug (Westeuropäische Normalzeit)'); + ok('"19-Sep-2015 16:11:07 +0000"' eq good_date('Date: 2015/09/19 16:11:07 '), 'good_date from RCS date' ) ; + + note( 'Leaving tests_good_date()' ) ; + return ; +} + + +sub tests_list_keys_in_2_not_in_1 +{ + note( 'Entering tests_list_keys_in_2_not_in_1()' ) ; + + + my @list; + ok( ! list_keys_in_2_not_in_1( {}, {}), 'list_keys_in_2_not_in_1: {} {}'); + ok( 0 == compare_lists( [], [ list_keys_in_2_not_in_1( {}, {} ) ] ), 'list_keys_in_2_not_in_1: {} {}'); + ok( 0 == compare_lists( ['a','b'], [ list_keys_in_2_not_in_1( {}, {'a' => 1, 'b' => 1}) ]), 'list_keys_in_2_not_in_1: {} {a, b}'); + ok( 0 == compare_lists( ['b'], [ list_keys_in_2_not_in_1( {'a' => 1}, {'a' => 1, 'b' => 1}) ]), 'list_keys_in_2_not_in_1: {a} {a, b}'); + ok( 0 == compare_lists( [], [ list_keys_in_2_not_in_1( {'a' => 1, 'b' => 1}, {'a' => 1, 'b' => 1}) ]), 'list_keys_in_2_not_in_1: {a, b} {a, b}'); + ok( 0 == compare_lists( [], [ list_keys_in_2_not_in_1( {'a' => 1, 'b' => 1, 'c' => 1}, {'a' => 1, 'b' => 1}) ]), 'list_keys_in_2_not_in_1: {a, b, c} {a, b}'); + ok( 0 == compare_lists( ['b'], [ list_keys_in_2_not_in_1( {'a' => 1, 'c' => 1}, {'a' => 1, 'b' => 1}) ]), 'list_keys_in_2_not_in_1: {a, b, c} {a, b}'); + + note( 'Leaving tests_list_keys_in_2_not_in_1()' ) ; + return ; +} + +sub list_keys_in_2_not_in_1 +{ + my $hash_1_ref = shift; + my $hash_2_ref = shift; + my @list; + + foreach my $key ( sort keys %{ $hash_2_ref } ) { + #$sync->{ debug } and print "$key\n" ; + if ( exists $hash_1_ref->{$key} ) + { + next ; + } + #$sync->{ debug } and print "list_keys_in_2_not_in_1: $key\n" ; + push @list, $key ; + } + #$sync->{ debug } and print "@list\n" ; + return( @list ) ; +} + + +sub list_folders_in_2_not_in_1 +{ + + my ( @h2_folders_not_in_h1, %h2_folders_not_in_h1 ) ; + @h2_folders_not_in_h1 = list_keys_in_2_not_in_1( \%h1_folders_all, \%h2_folders_all ) ; + map { $h2_folders_not_in_h1{$_} = 1} @h2_folders_not_in_h1 ; + @h2_folders_not_in_h1 = list_keys_in_2_not_in_1( \%h2_folders_from_1_all, \%h2_folders_not_in_h1 ) ; + #$sync->{ debug } and print "h2_folders_not_in_h1: @h2_folders_not_in_h1\n" ; + return( reverse @h2_folders_not_in_h1 ) ; +} + +sub tests_nb_messages_in_2_not_in_1 +{ + note( 'Entering tests_stats_across_folders()' ) ; + is( undef, nb_messages_in_2_not_in_1( ), 'nb_messages_in_2_not_in_1: no args => undef' ) ; + + my $mysync->{ h1_folders_of_md5 }->{ 'some_id_01' }->{ 'some_folder_01' } = 1 ; + is( 0, nb_messages_in_2_not_in_1( $mysync ), 'nb_messages_in_2_not_in_1: no messages in 2 => 0' ) ; + + $mysync->{ h1_folders_of_md5 }->{ 'some_id_in_1_and_2' }->{ 'some_folder_01' } = 2 ; + $mysync->{ h2_folders_of_md5 }->{ 'some_id_in_1_and_2' }->{ 'some_folder_02' } = 4 ; + + is( 0, nb_messages_in_2_not_in_1( $mysync ), 'nb_messages_in_2_not_in_1: a common message => 0' ) ; + + $mysync->{ h2_folders_of_md5 }->{ 'some_id_in_2_not_in_1' }->{ 'some_folder_02' } = 1 ; + is( 1, nb_messages_in_2_not_in_1( $mysync ), 'nb_messages_in_2_not_in_1: one message in_2_not_in_1 => 1' ) ; + + $mysync->{ h2_folders_of_md5 }->{ 'some_other_id_in_2_not_in_1' }->{ 'some_folder_02' } = 3 ; + is( 2, nb_messages_in_2_not_in_1( $mysync ), 'nb_messages_in_2_not_in_1: two messages in_2_not_in_1 => 2' ) ; + + note( 'Leaving tests_stats_across_folders()' ) ; + return ; +} + +sub nb_messages_in_2_not_in_1 +{ + my $mysync = shift ; + if ( not defined $mysync ) { return ; } + + $mysync->{ nb_messages_in_2_not_in_1 } = scalar( + list_keys_in_2_not_in_1( + $mysync->{ h1_folders_of_md5 }, + $mysync->{ h2_folders_of_md5 } ) ) ; + + return $mysync->{ nb_messages_in_2_not_in_1 } ; +} + + +sub nb_messages_in_1_not_in_2 +{ + my $mysync = shift ; + if ( not defined $mysync ) { return ; } + + $mysync->{ nb_messages_in_1_not_in_2 } = scalar( + list_keys_in_2_not_in_1( + $mysync->{ h2_folders_of_md5 }, + $mysync->{ h1_folders_of_md5 } ) ) ; + + return $mysync->{ nb_messages_in_1_not_in_2 } ; +} + + + +sub comment_on_final_diff_in_1_not_in_2 +{ + my $mysync = shift ; + + if ( not defined $mysync + or $mysync->{ justfolders } + or $mysync->{ useuid } + ) + { + return ; + } + + my $nb_identified_h1_messages = scalar( keys %{ $mysync->{ h1_folders_of_md5 } } ) ; + my $nb_identified_h2_messages = scalar( keys %{ $mysync->{ h2_folders_of_md5 } } ) ; + $mysync->{ debug } and myprint( "nb_keys h1_folders_of_md5 $nb_identified_h1_messages\n" ) ; + $mysync->{ debug } and myprint( "nb_keys h2_folders_of_md5 $nb_identified_h2_messages\n" ) ; + + if ( 0 == $nb_identified_h1_messages ) { return ; } + + # Calculate if not yet done + if ( not defined $mysync->{ nb_messages_in_1_not_in_2 } ) + { + nb_messages_in_1_not_in_2( $mysync ) ; + } + + + if ( 0 == $mysync->{ nb_messages_in_1_not_in_2 } ) + { + myprint( "The sync looks good, all ", + $nb_identified_h1_messages, + " identified messages in host1 are on host2.\n" ) ; + } + else + { + myprint( "The sync is not finished, there are ", + $mysync->{ nb_messages_in_1_not_in_2 }, + " among ", + $nb_identified_h1_messages, + " identified messages in host1 that are not on host2.\n" ) ; + } + + + if ( 1 <= $mysync->{ h1_nb_msg_noheader } ) + { + myprint( "There are ", + $mysync->{ h1_nb_msg_noheader }, + " unidentified messages (usually Sent or Draft messages).", + " To sync them add option --addheader\n" ) ; + } + else + { + myprint( "There is no unidentified message on host1.\n" ) ; + } + + return ; +} + +sub comment_on_final_diff_in_2_not_in_1 +{ + my $mysync = shift ; + + if ( not defined $mysync + or $mysync->{ justfolders } + or $mysync->{ useuid } + ) + { + return ; + } + + my $nb_identified_h2_messages = scalar( keys %{ $mysync->{ h2_folders_of_md5 } } ) ; + # Calculate if not done yet + if ( not defined $mysync->{ nb_messages_in_2_not_in_1 } ) + { + nb_messages_in_2_not_in_1( $mysync ) ; + } + + if ( 0 == $mysync->{ nb_messages_in_2_not_in_1 } ) + { + myprint( "The sync is strict, all ", + $nb_identified_h2_messages, + " identified messages in host2 are on host1.\n" ) ; + } + else + { + myprint( "The sync is not strict, there are ", + $mysync->{ nb_messages_in_2_not_in_1 }, + " among ", + $nb_identified_h2_messages, + " identified messages in host2 that are not on host1.", + " Use --delete2 and sync again to delete them and have a strict sync.\n" + ) ; + } + return ; +} + + +sub tests_match +{ + note( 'Entering tests_match()' ) ; + + # undef serie + is( undef, match( ), 'match: no args => undef' ) ; + is( undef, match( 'lalala' ), 'match: one args => undef' ) ; + + # This one gives 0 under a binary made by pp + # but 1 under "normal" Perl interpreter. So a PAR bug? + #is( 1, match( q{}, q{} ), 'match: q{} =~ q{} => 1' ) ; + + is( 'lalala', match( 'lalala', 'lalala' ), 'match: lalala =~ lalala => lalala' ) ; + is( 'lalala', match( 'lalala', '^lalala' ), 'match: lalala =~ ^lalala => lalala' ) ; + is( 'lalala', match( 'lalala', 'lalala$' ), 'match: lalala =~ lalala$ => lalala' ) ; + is( 'lalala', match( 'lalala', '^lalala$' ), 'match: lalala =~ ^lalala$ => lalala' ) ; + is( '_lalala_', match( '_lalala_', 'lalala' ), 'match: _lalala_ =~ lalala => _lalala_' ) ; + is( 'lalala', match( 'lalala', '.*' ), 'match: lalala =~ .* => lalala' ) ; + is( 'lalala', match( 'lalala', '.' ), 'match: lalala =~ . => lalala' ) ; + is( '/lalala/', match( '/lalala/', '/lalala/' ), 'match: /lalala/ =~ /lalala/ => /lalala/' ) ; + + is( 0, match( 'foo', 's/foo/bar/g' ), 'match: foo =~ s/foo/bar/g => 0' ) ; + is( 's/foo/bar/g', match( 's/foo/bar/g', 's/foo/bar/g' ), 'match: s/foo/bar/g =~ s/foo/bar/g => s/foo/bar/g' ) ; + + + is( 0, match( 'lalala', 'ooo' ), 'match: lalala =~ ooo => 0' ) ; + is( 0, match( 'lalala', 'lal_ala' ), 'match: lalala =~ lal_ala => 0' ) ; + is( 0, match( 'lalala', '\.' ), 'match: lalala =~ \. => 0' ) ; + is( 0, match( 'lalalaX', '^lalala$' ), 'match: lalalaX =~ ^lalala$ => 0' ) ; + is( 0, match( 'lalala', '/lalala/' ), 'match: lalala =~ /lalala/ => 0' ) ; + + is( 'LALALA', match( 'LALALA', '(?i:lalala)' ), 'match: LALALA =~ (?i:lalala) => 1' ) ; + + is( undef, match( 'LALALA', '(?{`ls /`})' ), 'match: LALALA =~ (?{`ls /`}) => undef' ) ; + is( undef, match( 'LALALA', '(?{print "CACA"})' ), 'match: LALALA =~ (?{print "CACA"}) => undef' ) ; + is( undef, match( 'CACA', '(??{print "CACA"})' ), 'match: CACA =~ (??{print "CACA"}) => undef' ) ; + + note( 'Leaving tests_match()' ) ; + + return ; +} + +sub match +{ + my( $var, $regex ) = @ARG ; + + # undef cases + if ( ( ! defined $var ) or ( ! defined $regex ) ) { return ; } + + # normal cases + if ( eval { $var =~ qr{$regex} } ) { + return $var ; + }elsif ( $EVAL_ERROR ) { + myprint( "Fatal regex $regex\n" ) ; + return ; + } else { + return 0 ; + } + return ; +} + + +sub tests_notmatch +{ + note( 'Entering tests_notmatch()' ) ; + + # undef serie + is( undef, notmatch( ), 'notmatch: no args => undef' ) ; + is( undef, notmatch( 'lalala' ), 'notmatch: one args => undef' ) ; + + is( 1, notmatch( 'lalala', '/lalala/' ), 'notmatch: lalala !~ /lalala/ => 1' ) ; + is( 0, notmatch( '/lalala/', '/lalala/' ), 'notmatch: /lalala/ !~ /lalala/ => 0' ) ; + is( 1, notmatch( 'lalala', '/ooo/' ), 'notmatch: lalala !~ /ooo/ => 1' ) ; + + # This one gives 1 under a binary made by pp + # but 0 under "normal" Perl interpreter. So a PAR bug, same in tests_match . + #is( 0, notmatch( q{}, q{} ), 'notmatch: q{} !~ q{} => 0' ) ; + + is( 0, notmatch( 'lalala', 'lalala' ), 'notmatch: lalala !~ lalala => 0' ) ; + is( 0, notmatch( 'lalala', '^lalala' ), 'notmatch: lalala !~ ^lalala => 0' ) ; + is( 0, notmatch( 'lalala', 'lalala$' ), 'notmatch: lalala !~ lalala$ => 0' ) ; + is( 0, notmatch( 'lalala', '^lalala$' ), 'notmatch: lalala !~ ^lalala$ => 0' ) ; + is( 0, notmatch( '_lalala_', 'lalala' ), 'notmatch: _lalala_ !~ lalala => 0' ) ; + is( 0, notmatch( 'lalala', '.*' ), 'notmatch: lalala !~ .* => 0' ) ; + is( 0, notmatch( 'lalala', '.' ), 'notmatch: lalala !~ . => 0' ) ; + + + is( 1, notmatch( 'lalala', 'ooo' ), 'notmatch: does not match regex => 1' ) ; + is( 1, notmatch( 'lalala', 'lal_ala' ), 'notmatch: does not match regex => 1' ) ; + is( 1, notmatch( 'lalala', '\.' ), 'notmatch: matches regex => 0' ) ; + is( 1, notmatch( 'lalalaX', '^lalala$' ), 'notmatch: does not match regex => 1' ) ; + + note( 'Leaving tests_notmatch()' ) ; + + return ; +} + +sub notmatch +{ + my( $var, $regex ) = @ARG ; + + # undef cases + if ( ( ! defined $var ) or ( ! defined $regex ) ) { return ; } + + # normal cases + if ( eval { $var !~ $regex } ) { + return 1 ; + }elsif ( $EVAL_ERROR ) { + myprint( "Fatal regex $regex\n" ) ; + return ; + }else{ + return 0 ; + } + return ; +} + + +sub delete_folders_in_2_not_in_1 +{ + + foreach my $folder ( @h2_folders_not_in_1 ) { + if ( defined $delete2foldersonly and eval "\$folder !~ $delete2foldersonly" ) { + myprint( "Not deleting $folder because of --delete2foldersonly $delete2foldersonly\n" ) ; + next ; + } + if ( defined $delete2foldersbutnot and eval "\$folder =~ $delete2foldersbutnot" ) { + myprint( "Not deleting $folder because of --delete2foldersbutnot $delete2foldersbutnot\n" ) ; + next ; + } + my $res = $sync->{dry} ; # always success in dry mode! + $sync->{imap2}->unsubscribe( $folder ) if ( ! $sync->{dry} ) ; + $res = $sync->{imap2}->delete( $folder ) if ( ! $sync->{dry} ) ; + if ( $res ) { + myprint( "Deleted $folder", "$sync->{dry_message}", "\n" ) ; + }else{ + myprint( "Deleting $folder failed", "\n" ) ; + } + } + return ; +} + +sub delete_folder +{ + my ( $mysync, $imap, $folder, $Side ) = @_ ; + if ( ! $mysync ) { return ; } + if ( ! $imap ) { return ; } + if ( ! $folder ) { return ; } + $Side ||= 'HostX' ; + + my $res = $mysync->{dry} ; # always success in dry mode! + if ( ! $mysync->{dry} ) { + $imap->unsubscribe( $folder ) ; + $res = $imap->delete( $folder ) ; + } + if ( $res ) { + myprint( "$Side deleted $folder", $mysync->{dry_message}, "\n" ) ; + return 1 ; + }else{ + myprint( "$Side deleting $folder failed", "\n" ) ; + return ; + } +} + +sub delete1emptyfolders +{ + my $mysync = shift ; + if ( ! $mysync ) { return ; } # abort if no parameter + if ( ! $mysync->{delete1emptyfolders} ) { return ; } # abort if --delete1emptyfolders off + my $imap = $mysync->{imap1} ; + if ( ! $imap ) { return ; } # abort if no imap + if ( $imap->IsUnconnected( ) ) { return ; } # abort if disconnected + + my %folders_kept ; + myprint( qq{Host1 deleting empty folders\n} ) ; + foreach my $folder ( reverse sort @{ $mysync->{h1_folders_wanted} } ) { + my $parenthood = $imap->is_parent( $folder ) ; + if ( defined $parenthood and $parenthood ) { + myprint( "Host1: folder $folder has subfolders\n" ) ; + $folders_kept{ $folder }++ ; + next ; + } + my $nb_messages_select = examine_folder_and_count( $mysync, $imap, $folder, 'Host1' ) ; + if ( ! defined $nb_messages_select ) { next ; } # Select failed => Neither continue nor keep this folder } + my $nb_messages_search = scalar( @{ $imap->messages( ) } ) ; + if ( 0 != $nb_messages_select and 0 != $nb_messages_search ) { + myprint( "Host1: folder $folder has messages: $nb_messages_search (search) $nb_messages_select (select)\n" ) ; + $folders_kept{ $folder }++ ; + next ; + } + if ( 0 != $nb_messages_select + $nb_messages_search ) { + myprint( "Host1: folder $folder odd messages count: $nb_messages_search (search) $nb_messages_select (select)\n" ) ; + $folders_kept{ $folder }++ ; + next ; + } + # Here we must have 0 messages by messages() aka "SEARCH ALL" and also "EXAMINE" + if ( uc $folder eq 'INBOX' ) { + myprint( "Host1: Not deleting $folder\n" ) ; + $folders_kept{ $folder }++ ; + next ; + } + myprint( "Host1: deleting empty folder $folder\n" ) ; + # can not delete a SELECTed or EXAMINEd folder so closing it + # could changed be SELECT INBOX + $imap->close( ) ; # close after examine does not expunge; anyway expunging an empty folder... + if ( delete_folder( $mysync, $imap, $folder, 'Host1' ) ) { + next ; # Deleted, good! + }else{ + $folders_kept{ $folder }++ ; + next ; # Not deleted, bad! + } + } + remove_deleted_folders_from_wanted_list( $mysync, %folders_kept ) ; + myprint( qq{Host1 ended deleting empty folders\n} ) ; + return ; +} + +sub remove_deleted_folders_from_wanted_list +{ + my ( $mysync, %folders_kept ) = @ARG ; + + my @h1_folders_wanted_init = @{ $mysync->{h1_folders_wanted} } ; + my @h1_folders_wanted_last ; + foreach my $folder ( @h1_folders_wanted_init ) { + if ( $folders_kept{ $folder } ) { + push @h1_folders_wanted_last, $folder ; + } + } + @{ $mysync->{h1_folders_wanted} } = @h1_folders_wanted_last ; + return ; +} + + +sub examine_folder_and_count +{ + my ( $mysync, $imap, $folder, $Side ) = @_ ; + $Side ||= 'HostX' ; + + if ( ! examine_folder( $mysync, $imap, $folder, $Side ) ) { + return ; + } + my $nb_messages_select = count_from_select( $imap->History ) ; + return $nb_messages_select ; +} + + +sub tests_delete1emptyfolders +{ + note( 'Entering tests_delete1emptyfolders()' ) ; + + + is( undef, delete1emptyfolders( ), q{delete1emptyfolders: undef} ) ; + my $syncT ; + is( undef, delete1emptyfolders( $syncT ), q{delete1emptyfolders: undef 2} ) ; + my $imapT ; + $syncT->{imap1} = $imapT ; + is( undef, delete1emptyfolders( $syncT ), q{delete1emptyfolders: undef imap} ) ; + + require_ok( "Test::MockObject" ) ; + $imapT = Test::MockObject->new( ) ; + $syncT->{imap1} = $imapT ; + + $imapT->set_true( 'IsUnconnected' ) ; + is( undef, delete1emptyfolders( $syncT ), q{delete1emptyfolders: Unconnected imap} ) ; + + # Now connected tests + $imapT->set_false( 'IsUnconnected' ) ; + $imapT->mock( 'LastError', sub { q{LastError mocked} } ) ; + + $syncT->{delete1emptyfolders} = 0 ; + tests_delete1emptyfolders_unit( + $syncT, + [ qw{ INBOX DELME1 DELME2 } ], + [ qw{ INBOX DELME1 DELME2 } ], + q{tests_delete1emptyfolders: --delete1emptyfolders OFF} + ) ; + + # All are parents => no deletion at all + $imapT->set_true( 'is_parent' ) ; + $syncT->{delete1emptyfolders} = 1 ; + tests_delete1emptyfolders_unit( + $syncT, + [ qw{ INBOX DELME1 DELME2 } ], + [ qw{ INBOX DELME1 DELME2 } ], + q{tests_delete1emptyfolders: --delete1emptyfolders ON} + ) ; + + # No parents but examine false for all => skip all + $imapT->set_false( 'is_parent', 'examine' ) ; + + tests_delete1emptyfolders_unit( + $syncT, + [ qw{ INBOX DELME1 DELME2 } ], + [ ], + q{tests_delete1emptyfolders: EXAMINE fails} + ) ; + + # examine ok for all but History bad => skip all + $imapT->set_true( 'examine' ) ; + $imapT->mock( 'History', sub { ( q{History badly mocked} ) } ) ; + tests_delete1emptyfolders_unit( + $syncT, + [ qw{ INBOX DELME1 DELME2 } ], + [ ], + q{tests_delete1emptyfolders: examine ok but History badly mocked so count messages fails} + ) ; + + # History good but some messages EXISTS == messages() => no deletion + $imapT->mock( 'History', sub { ( q{* 2 EXISTS} ) } ) ; + $imapT->mock( 'messages', sub { [ qw{ UID_1 UID_2 } ] } ) ; + tests_delete1emptyfolders_unit( + $syncT, + [ qw{ INBOX DELME1 DELME2 } ], + [ qw{ INBOX DELME1 DELME2 } ], + q{tests_delete1emptyfolders: History EXAMINE ok, several messages} + ) ; + + # 0 EXISTS but != messages() => no deletion + $imapT->mock( 'History', sub { ( q{* 0 EXISTS} ) } ) ; + $imapT->mock( 'messages', sub { [ qw{ UID_1 UID_2 } ] } ) ; + tests_delete1emptyfolders_unit( + $syncT, + [ qw{ INBOX DELME1 DELME2 } ], + [ qw{ INBOX DELME1 DELME2 } ], + q{tests_delete1emptyfolders: 0 EXISTS but 2 by messages()} + ) ; + + # 1 EXISTS but != 0 == messages() => no deletion + $imapT->mock( 'History', sub { ( q{* 1 EXISTS} ) } ) ; + $imapT->mock( 'messages', sub { [ ] } ) ; + tests_delete1emptyfolders_unit( + $syncT, + [ qw{ INBOX DELME1 DELME2 } ], + [ qw{ INBOX DELME1 DELME2 } ], + q{tests_delete1emptyfolders: 1 EXISTS but 0 by messages()} + ) ; + + # 0 EXISTS and 0 == messages() => deletion except INBOX + $imapT->mock( 'History', sub { ( q{* 0 EXISTS} ) } ) ; + $imapT->mock( 'messages', sub { [ ] } ) ; + $imapT->set_true( qw{ delete close unsubscribe } ) ; + $syncT->{dry_message} = q{ (not really since in a mocked test)} ; + tests_delete1emptyfolders_unit( + $syncT, + [ qw{ INBOX DELME1 DELME2 } ], + [ qw{ INBOX } ], + q{tests_delete1emptyfolders: 0 EXISTS 0 by messages() delete folders, keep INBOX} + ) ; + + note( 'Leaving tests_delete1emptyfolders()' ) ; + return ; +} + +sub tests_delete1emptyfolders_unit +{ + note( 'Entering tests_delete1emptyfolders_unit()' ) ; + + my $syncT = shift ; + my $folders1wanted_init_ref = shift ; + my $folders1wanted_after_ref = shift ; + my $comment = shift || q{delete1emptyfolders:} ; + + my @folders1wanted_init = @{ $folders1wanted_init_ref } ; + my @folders1wanted_after = @{ $folders1wanted_after_ref } ; + + @{ $syncT->{h1_folders_wanted} } = @folders1wanted_init ; + + is_deeply( $syncT->{h1_folders_wanted}, \@folders1wanted_init, qq{$comment, init check} ) ; + delete1emptyfolders( $syncT ) ; + is_deeply( $syncT->{h1_folders_wanted}, \@folders1wanted_after, qq{$comment, after check} ) ; + + note( 'Leaving tests_delete1emptyfolders_unit()' ) ; + return ; +} + +sub extract_header +{ + my $string = shift ; + + my ( $header ) = split /\n\n/x, $string ; + if ( ! $header ) { return( q{} ) ; } + #myprint( "[$header]\n" ) ; + return( $header ) ; +} + +sub tests_extract_header +{ + note( 'Entering tests_extract_header()' ) ; + +my $h = <<'EOM'; +Message-Id: <20100428101817.A66CB162474E@plume.est.belle> +Date: Wed, 28 Apr 2010 12:18:17 +0200 (CEST) +From: gilles@louloutte.dyndns.org (Gilles LAMIRAL) +EOM +chomp $h ; +ok( $h eq extract_header( +<<'EOM' +Message-Id: <20100428101817.A66CB162474E@plume.est.belle> +Date: Wed, 28 Apr 2010 12:18:17 +0200 (CEST) +From: gilles@louloutte.dyndns.org (Gilles LAMIRAL) + +body +lalala +EOM +), 'extract_header: 1') ; + + + + note( 'Leaving tests_extract_header()' ) ; + return ; +} + +sub decompose_header{ + my $string = shift ; + + # a hash, for a keyword header KEY value are list of strings [VAL1, VAL1_other, etc] + # Think of multiple "Received:" header lines. + my $header = { } ; + + my ($key, $val ) ; + my @line = split /\n|\r\n/x, $string ; + foreach my $line ( @line ) { + #myprint( "DDD $line\n" ) ; + # End of header + last if ( $line =~ m{^$}xo ) ; + # Key: value + if ( $line =~ m/(^[^:]+):\s(.*)/xo ) { + $key = $1 ; + $val = $2 ; + $debugdev and myprint( "DDD KV [$key] [$val]\n" ) ; + push @{ $header->{ $key } }, $val ; + # blanc and value => value from previous line continues + }elsif( $line =~ m/^(\s+)(.*)/xo ) { + $val = $2 ; + $debugdev and myprint( "DDD V [$val]\n" ) ; + @{ $header->{ $key } }[ $LAST ] .= " $val" if $key ; + # dirty line? + }else{ + next ; + } + } + + #myprint( Data::Dumper->Dump( [ $header ] ) ) ; + + return( $header ) ; +} + + +sub tests_decompose_header{ + note( 'Entering tests_decompose_header()' ) ; + + + my $header_dec ; + + $header_dec = decompose_header( +<<'EOH' +KEY_1: VAL_1 +KEY_2: VAL_2 + VAL_2_+ + VAL_2_++ +KEY_3: VAL_3 +KEY_1: VAL_1_other +KEY_4: VAL_4 + VAL_4_+ +KEY_5 BLANC: VAL_5 + +KEY_6_BAD_BODY: VAL_6 +EOH + ) ; + + ok( 'VAL_3' + eq $header_dec->{ 'KEY_3' }[0], 'decompose_header: VAL_3' ) ; + + ok( 'VAL_1' + eq $header_dec->{ 'KEY_1' }[0], 'decompose_header: VAL_1' ) ; + + ok( 'VAL_1_other' + eq $header_dec->{ 'KEY_1' }[1], 'decompose_header: VAL_1_other' ) ; + + ok( 'VAL_2 VAL_2_+ VAL_2_++' + eq $header_dec->{ 'KEY_2' }[0], 'decompose_header: VAL_2 VAL_2_+ VAL_2_++' ) ; + + ok( 'VAL_4 VAL_4_+' + eq $header_dec->{ 'KEY_4' }[0], 'decompose_header: VAL_4 VAL_4_+' ) ; + + ok( ' VAL_5' + eq $header_dec->{ 'KEY_5 BLANC' }[0], 'decompose_header: KEY_5 BLANC' ) ; + + ok( not( defined $header_dec->{ 'KEY_6_BAD_BODY' }[0] ), 'decompose_header: KEY_6_BAD_BODY' ) ; + + + $header_dec = decompose_header( +<<'EOH' +Message-Id: <20100428101817.A66CB162474E@plume.est.belle> +Date: Wed, 28 Apr 2010 12:18:17 +0200 (CEST) +From: gilles@louloutte.dyndns.org (Gilles LAMIRAL) +EOH + ) ; + + ok( '<20100428101817.A66CB162474E@plume.est.belle>' + eq $header_dec->{ 'Message-Id' }[0], 'decompose_header: 1' ) ; + + $header_dec = decompose_header( +<<'EOH' +Return-Path: +Received: by plume.est.belle (Postfix, from userid 1000) + id 120A71624742; Wed, 28 Apr 2010 01:46:40 +0200 (CEST) +Subject: test:eekahceishukohpe +EOH +) ; + ok( +'by plume.est.belle (Postfix, from userid 1000) id 120A71624742; Wed, 28 Apr 2010 01:46:40 +0200 (CEST)' + eq $header_dec->{ 'Received' }[0], 'decompose_header: 2' ) ; + + $header_dec = decompose_header( +<<'EOH' +Received: from plume (localhost [127.0.0.1]) + by plume.est.belle (Postfix) with ESMTP id C6EB73F6C9 + for ; Mon, 26 Nov 2007 10:39:06 +0100 (CET) +Received: from plume [192.168.68.7] + by plume with POP3 (fetchmail-6.3.6) + for (single-drop); Mon, 26 Nov 2007 10:39:06 +0100 (CET) +EOH + ) ; + ok( + 'from plume (localhost [127.0.0.1]) by plume.est.belle (Postfix) with ESMTP id C6EB73F6C9 for ; Mon, 26 Nov 2007 10:39:06 +0100 (CET)' + eq $header_dec->{ 'Received' }[0], 'decompose_header: 3' ) ; + ok( + 'from plume [192.168.68.7] by plume with POP3 (fetchmail-6.3.6) for (single-drop); Mon, 26 Nov 2007 10:39:06 +0100 (CET)' + eq $header_dec->{ 'Received' }[1], 'decompose_header: 3' ) ; + +# Bad header beginning with a blank character + $header_dec = decompose_header( +<<'EOH' + KEY_1: VAL_1 +KEY_2: VAL_2 + VAL_2_+ + VAL_2_++ +KEY_3: VAL_3 +KEY_1: VAL_1_other +EOH + ) ; + + ok( 'VAL_3' + eq $header_dec->{ 'KEY_3' }[0], 'decompose_header: Bad header VAL_3' ) ; + + ok( 'VAL_1_other' + eq $header_dec->{ 'KEY_1' }[0], 'decompose_header: Bad header VAL_1_other' ) ; + + ok( 'VAL_2 VAL_2_+ VAL_2_++' + eq $header_dec->{ 'KEY_2' }[0], 'decompose_header: Bad header VAL_2 VAL_2_+ VAL_2_++' ) ; + + note( 'Leaving tests_decompose_header()' ) ; + return ; +} + +sub tests_epoch +{ + note( 'Entering tests_epoch()' ) ; + + ok( '1282658400' eq epoch( '24-Aug-2010 16:00:00 +0200' ), 'epoch 24-Aug-2010 16:00:00 +0200 -> 1282658400' ) ; + ok( '1282658400' eq epoch( '24-Aug-2010 14:00:00 +0000' ), 'epoch 24-Aug-2010 14:00:00 +0000 -> 1282658400' ) ; + ok( '1282658400' eq epoch( '24-Aug-2010 12:00:00 -0200' ), 'epoch 24-Aug-2010 12:00:00 -0200 -> 1282658400' ) ; + ok( '1282658400' eq epoch( '24-Aug-2010 16:01:00 +0201' ), 'epoch 24-Aug-2010 16:01:00 +0201 -> 1282658400' ) ; + ok( '1282658400' eq epoch( '24-Aug-2010 14:01:00 +0001' ), 'epoch 24-Aug-2010 14:01:00 +0001 -> 1282658400' ) ; + + ok( '1280671200' eq epoch( '1-Aug-2010 16:00:00 +0200' ), 'epoch 1-Aug-2010 16:00:00 +0200 -> 1280671200' ) ; + ok( '1280671200' eq epoch( '1-Aug-2010 14:00:00 +0000' ), 'epoch 1-Aug-2010 14:00:00 +0000 -> 1280671200' ) ; + ok( '1280671200' eq epoch( '1-Aug-2010 12:00:00 -0200' ), 'epoch 1-Aug-2010 12:00:00 -0200 -> 1280671200' ) ; + ok( '1280671200' eq epoch( '1-Aug-2010 16:01:00 +0201' ), 'epoch 1-Aug-2010 16:01:00 +0201 -> 1280671200' ) ; + ok( '1280671200' eq epoch( '1-Aug-2010 14:01:00 +0001' ), 'epoch 1-Aug-2010 14:01:00 +0001 -> 1280671200' ) ; + + is( '1280671200', epoch( '1-Aug-2010 14:01:00 +0001' ), 'epoch 1-Aug-2010 14:01:00 +0001 -> 1280671200' ) ; + is( '946684800', epoch( '00-Jan-0000 00:00:00 +0000' ), 'epoch 1-Aug-2010 14:01:00 +0001 -> 1280671200' ) ; + + note( 'Leaving tests_epoch()' ) ; + return ; +} + +sub epoch +{ + # incoming format: + # internal date 24-Aug-2010 16:00:00 +0200 + + # outgoing format: epoch + + + my $d = shift ; + return(q{}) if not defined $d; + + my ( $mday, $month, $year, $hour, $min, $sec, $sign, $zone_h, $zone_m ) ; + my $time ; + + if ( $d =~ m{(\d{1,2})-([A-Z][a-z]{2})-(\d{4})\s(\d{2}):(\d{2}):(\d{2})\s((?:\+|-))(\d{2})(\d{2})}xo ) { + #myprint( "internal: [$1][$2][$3][$4][$5][$6][$7][$8][$9]\n" ) ; + ( $mday, $month, $year, $hour, $min, $sec, $sign, $zone_h, $zone_m ) + = ( $1, $2, $3, $4, $5, $6, $7, $8, $9 ) ; + #myprint( "( $mday, $month, $year, $hour, $min, $sec, $sign, $zone_h, $zone_m )\n" ) ; + + $sign = +1 if ( '+' eq $sign ) ; + $sign = $MINUS_ONE if ( '-' eq $sign ) ; + + if ( 0 == $mday ) { + myprint( "buggy day in $d. Fixed to 01\n" ) ; + $mday = '01' ; + } + $time = timegm( $sec, $min, $hour, $mday, $month_abrev{$month}, $year ) + - $sign * ( 3600 * $zone_h + 60 * $zone_m ) ; + + #myprint( "$time ", scalar localtime($time), "\n"); + } + return( $time ) ; +} + +sub tests_add_header +{ + note( 'Entering tests_add_header()' ) ; + + ok( 'Message-Id: ' eq add_header(), 'add_header no arg' ) ; + ok( 'Message-Id: <123456789@imapsync>' eq add_header( '123456789' ), 'add_header 123456789' ) ; + + note( 'Leaving tests_add_header()' ) ; + return ; +} + +sub add_header +{ + my $header_uid = shift || 'mistake' ; + my $header_Message_Id = 'Message-Id: <' . $header_uid . '@imapsync>' ; + return( $header_Message_Id ) ; +} + + + + +sub tests_max_line_length +{ + note( 'Entering tests_max_line_length()' ) ; + + ok( 0 == max_line_length( q{} ), 'max_line_length: 0 == null string' ) ; + ok( 1 == max_line_length( "\n" ), 'max_line_length: 1 == \n' ) ; + ok( 1 == max_line_length( "\n\n" ), 'max_line_length: 1 == \n\n' ) ; + ok( 1 == max_line_length( "\n" x 500 ), 'max_line_length: 1 == 500 \n' ) ; + ok( 1 == max_line_length( 'a' ), 'max_line_length: 1 == a' ) ; + ok( 2 == max_line_length( "a\na" ), 'max_line_length: 2 == a\na' ) ; + ok( 2 == max_line_length( "a\na\n" ), 'max_line_length: 2 == a\na\n' ) ; + ok( 3 == max_line_length( "a\nab\n" ), 'max_line_length: 3 == a\nab\n' ) ; + ok( 3 == max_line_length( "a\nab\n" x 1_000 ), 'max_line_length: 3 == 1_000 a\nab\n' ) ; + ok( 3 == max_line_length( "a\nab\nabc" ), 'max_line_length: 3 == a\nab\nabc' ) ; + + ok( 4 == max_line_length( "a\nab\nabc\n" ), 'max_line_length: 4 == a\nab\nabc\n' ) ; + ok( 5 == max_line_length( "a\nabcd\nabc\n" ), 'max_line_length: 5 == a\nabcd\nabc\n' ) ; + ok( 5 == max_line_length( "a\nabcd\nabc\n\nabcd\nabcd\nabcd\nabcd\nabcd\nabcd\nabcd\nabcd" ), 'max_line_length: 5 == a\nabcd\nabc\n\nabcd\nabcd\nabcd\nabcd\nabcd\nabcd\nabcd\nabcd' ) ; + + note( 'Leaving tests_max_line_length()' ) ; + return ; +} + +sub max_line_length +{ + my $string = shift ; + my $max = 0 ; + + while ( $string =~ m/([^\n]*\n?)/msxg ) { + $max = max( $max, length $1 ) ; + } + return( $max ) ; +} + +sub set_checknoabletosearch +{ + my $mysync = shift @ARG ; + if ( defined $mysync->{ checknoabletosearch } ) + { + return ; + } + elsif ( $mysync->{ justfolders } ) + { + $mysync->{ checknoabletosearch } = 0 ; + } + else + { + $mysync->{ checknoabletosearch } = 1 ; + } + return ; +} + + +sub tests_setlogfile +{ + note( 'Entering tests_setlogfile()' ) ; + + my $mysync = {} ; + $mysync->{logdir} = 'vallogdir' ; + $mysync->{logfile} = 'vallogfile.txt' ; + is( 'vallogdir/vallogfile.txt', setlogfile( $mysync ), + 'setlogfile: logdir vallogdir, logfile vallogfile.txt, vallogdir/vallogfile.txt' ) ; + + SKIP: { + skip( 'Too hard to have a well known timezone on Windows', 9 ) if ( 'MSWin32' eq $OSNAME ) ; + + local $ENV{TZ} = 'GMT' ; + + $mysync = { + timestart => 2, + } ; + + is( "$DEFAULT_LOGDIR/1970_01_01_00_00_02_000__.txt", setlogfile( $mysync ), + "setlogfile: default is like $DEFAULT_LOGDIR/1970_01_01_00_00_02_000__.txt" ) ; + + $mysync = { + timestart => 2, + user1 => 'user1', + user2 => 'user2', + abort => 1, + } ; + + is( "$DEFAULT_LOGDIR/1970_01_01_00_00_02_000_user1_user2_abort.txt", setlogfile( $mysync ), + "setlogfile: default is like $DEFAULT_LOGDIR/1970_01_01_00_00_02_000_user1_user2_abort.txt" ) ; + + $mysync = { + timestart => 2, + user1 => 'user1', + user2 => 'user2', + remote => 'zzz', + } ; + + is( "$DEFAULT_LOGDIR/1970_01_01_00_00_02_000_user1_user2_remote.txt", setlogfile( $mysync ), + "setlogfile: default is like $DEFAULT_LOGDIR/1970_01_01_00_00_02_000_user1_user2_remote.txt" ) ; + + $mysync = { + timestart => 2, + user1 => 'user1', + user2 => 'user2', + remote => 'zzz', + abort => 1, + } ; + + is( "$DEFAULT_LOGDIR/1970_01_01_00_00_02_000_user1_user2_remote_abort.txt", setlogfile( $mysync ), + "setlogfile: default is like $DEFAULT_LOGDIR/1970_01_01_00_00_02_000_user1_user2_remote_abort.txt" ) ; + + + $mysync = { + timestart => 2, + user1 => 'user1', + user2 => 'user2', + } ; + + is( "$DEFAULT_LOGDIR/1970_01_01_00_00_02_000_user1_user2.txt", setlogfile( $mysync ), + "setlogfile: default is like $DEFAULT_LOGDIR/1970_01_01_00_00_02_000_user1_user2.txt" ) ; + + $mysync->{logdir} = undef ; + $mysync->{logfile} = undef ; + is( "$DEFAULT_LOGDIR/1970_01_01_00_00_02_000_user1_user2.txt", setlogfile( $mysync ), + "setlogfile: logdir undef, $DEFAULT_LOGDIR/1970_01_01_00_00_02_000_user1_user2.txt" ) ; + + $mysync->{logdir} = q{} ; + $mysync->{logfile} = undef ; + is( '1970_01_01_00_00_02_000_user1_user2.txt', setlogfile( $mysync ), + 'setlogfile: logdir empty, 1970_01_01_00_00_02_000_user1_user2.txt' ) ; + + $mysync->{logdir} = 'vallogdir' ; + $mysync->{logfile} = undef ; + is( 'vallogdir/1970_01_01_00_00_02_000_user1_user2.txt', setlogfile( $mysync ), + 'setlogfile: logdir vallogdir, vallogdir/1970_01_01_00_00_02_000_user1_user2.txt' ) ; + + $mysync = { + user1 => 'us/er1a*|?:"<>b', + user2 => 'u/ser2a*|?:"<>b', + } ; + + is( "$DEFAULT_LOGDIR/1970_01_01_00_00_00_000_us_er1a_______b_u_ser2a_______b.txt", setlogfile( $mysync ), + "setlogfile: logdir undef, $DEFAULT_LOGDIR/1970_01_01_00_00_00_000_us_er1a_______b_u_ser2a_______b.txt" ) ; + + + + } ; + + note( 'Leaving tests_setlogfile()' ) ; + return ; +} + +sub setlogfile +{ + my( $mysync ) = shift ; + + # When aborting another process the log file name finishes with "_abort.txt" + my $abort_suffix = ( $mysync->{ abort } ) ? '_abort' : q{} ; + + # When acting as a proxy the log file name finishes with "_remote.txt" + # proxy mode is not done in imapsync, it is done by proximapsync + my $remote_suffix = ( $mysync->{ remote } ) ? '_remote' : q{} ; + + my $suffix = ( + filter_forbidden_characters( slash_to_underscore( $mysync->{ user1 } ) ) || q{} ) + . '_' + . ( filter_forbidden_characters( slash_to_underscore( $mysync->{ user2 } ) ) || q{} ) + . $remote_suffix . $abort_suffix ; + + $mysync->{ logdir } = defined $mysync->{ logdir } ? $mysync->{ logdir } : $DEFAULT_LOGDIR ; + + $mysync->{ logfile } = defined $mysync->{ logfile } + ? "$mysync->{ logdir }/$mysync->{ logfile }" + : logfile( $mysync->{ timestart }, $suffix, $mysync->{ logdir } ) ; + + return( $mysync->{ logfile } ) ; +} + +sub tests_logfile +{ + note( 'Entering tests_logfile()' ) ; + + SKIP: { + # Too hard to have a well known timezone on Windows + skip( 'Too hard to have a well known timezone on Windows', 10 ) if ( 'MSWin32' eq $OSNAME ) ; + + local $ENV{TZ} = 'GMT' ; + { + POSIX::tzset unless ('MSWin32' eq $OSNAME) ; + is( '1970_01_01_00_00_00_000.txt', logfile( ), 'logfile: no args => 1970_01_01_00_00_00.txt' ) ; + is( '1970_01_01_00_00_00_000.txt', logfile( 0 ), 'logfile: 0 => 1970_01_01_00_00_00.txt' ) ; + is( '1970_01_01_00_01_01_000.txt', logfile( 61 ), 'logfile: 0 => 1970_01_01_00_01_01.txt' ) ; + is( '1970_01_01_00_01_01_234.txt', logfile( 61.234 ), 'logfile: 0 => 1970_01_01_00_01_01.txt' ) ; + is( '2010_08_24_14_00_00_000.txt', logfile( 1_282_658_400 ), 'logfile: 1_282_658_400 => 2010_08_24_14_00_00.txt' ) ; + is( '2010_08_24_14_01_01_000.txt', logfile( 1_282_658_461 ), 'logfile: 1_282_658_461 => 2010_08_24_14_01_01.txt' ) ; + is( '2010_08_24_14_01_01_000_poupinette.txt', logfile( 1_282_658_461, 'poupinette' ), 'logfile: 1_282_658_461 poupinette => 2010_08_24_14_01_01_poupinette.txt' ) ; + is( '2010_08_24_14_01_01_000_removeblanks.txt', logfile( 1_282_658_461, ' remove blanks ' ), 'logfile: 1_282_658_461 remove blanks => 2010_08_24_14_01_01_000_removeblanks' ) ; + + is( '2010_08_24_14_01_01_234_poup.txt', logfile( 1_282_658_461.2347, 'poup' ), + 'logfile: 1_282_658_461.2347 poup => 2010_08_24_14_01_01_234_poup.txt' ) ; + + is( 'dirdir/2010_08_24_14_01_01_234_poup.txt', logfile( 1_282_658_461.2347, 'poup', 'dirdir' ), + 'logfile: 1_282_658_461.2347 poup dirdir => dirdir/2010_08_24_14_01_01_234_poup.txt' ) ; + + + + } + POSIX::tzset unless ('MSWin32' eq $OSNAME) ; + } ; + + note( 'Leaving tests_logfile()' ) ; + return ; +} + + +sub logfile +{ + my ( $time, $suffix, $dir ) = @_ ; + + $time ||= 0 ; + $suffix ||= q{} ; + $suffix =~ tr/ //ds ; + my $sep_suffix = ( $suffix ) ? '_' : q{} ; + $dir ||= q{} ; + my $sep_dir = ( $dir ) ? '/' : q{} ; + + my $date_str = POSIX::strftime( '%Y_%m_%d_%H_%M_%S', localtime $time ) ; + # Because of ab tests or web accesses, more than one sync withing one second is possible + # so we add also milliseconds + $date_str .= sprintf "_%03d", ($time - int( $time ) ) * 1000 ; # without rounding + my $logfile = "${dir}${sep_dir}${date_str}${sep_suffix}${suffix}.txt" ; + return( $logfile ) ; +} + + +sub tests_localtimez +{ + note( 'Entering tests_localtimez()' ) ; + + SKIP: { + # Too hard to have a well known timezone on Windows + skip( 'Too hard to have a well known timezone on Windows', 1 ) if ( 'MSWin32' eq $OSNAME ) ; + local $ENV{TZ} = 'GMT' ; + like( localtimez( 0 ), qr'1970-01-01 00:00:00 \+0000 (GMT|UTC)', 'localtimez: 0 => match 1970-01-01 00:00:00 +0000 GMT' ) ; + } + + is( localtimez( ), localtimez( time ), 'localtimez: undef => equals currrent' ) ; + note( 'Leaving tests_localtimez()' ) ; + return ; +} + + + +sub localtimez +{ + my $time = shift ; + + $time = defined( $time ) ? $time : time ; + + my $datetimestr = POSIX::strftime( '%A %e %B %Y-%m-%d %H:%M:%S %z %Z', localtime( $time ) ) ; + + #myprint( "$datetimestr\n" ) ; + return $datetimestr ; +} + + + + +sub tests_slash_to_underscore +{ + note( 'Entering tests_slash_to_underscore()' ) ; + + is( undef, slash_to_underscore( ), 'slash_to_underscore: no parameters => undef' ) ; + is( '_', slash_to_underscore( '/' ), 'slash_to_underscore: / => _' ) ; + is( '_abc_def_', slash_to_underscore( '/abc/def/' ), 'slash_to_underscore: /abc/def/ => _abc_def_' ) ; + note( 'Leaving tests_slash_to_underscore()' ) ; + return ; +} + +sub slash_to_underscore +{ + my $string = shift ; + + if ( ! defined $string ) { return ; } + + $string =~ tr{/}{_} ; + + return( $string ) ; +} + + + + +sub tests_million_folders_baby_2 +{ + note( 'Entering tests_million_folders_baby_2()' ) ; + + my %long ; + @long{ 1 .. 900_000 } = (1) x 900_000 ; + #myprint( %long, "\n" ) ; + my $pasglop = 0 ; + foreach my $elem ( 1 .. 900_000 ) { + #$debug and myprint( "$elem " ) ; + if ( not exists $long{ $elem } ) { + $pasglop++ ; + } + } + ok( 0 == $pasglop, 'tests_million_folders_baby_2: search among 900_000' ) ; + # myprint( "$pasglop\n" ) ; + + note( 'Leaving tests_million_folders_baby_2()' ) ; + return ; +} + + + +sub tests_always_fail +{ + note( 'Entering tests_always_fail()' ) ; + + is( 0, 1, 'always_fail: 0 is 1' ) ; + + note( 'Leaving tests_always_fail()' ) ; + return ; +} + + +sub tests_logfileprepa +{ + note( 'Entering tests_logfileprepa()' ) ; + + is( undef, logfileprepa( ), 'logfileprepa: no args => undef' ) ; + my $logfile = 'W/tmp/tests/tests_logfileprepa.txt' ; + is( 1, logfileprepa( $logfile ), 'logfileprepa: W/tmp/tests/tests_logfileprepa.txt => 1' ) ; + + note( 'Leaving tests_logfileprepa()' ) ; + return ; +} + +sub logfileprepa +{ + my $logfile = shift ; + + if ( ! defined( $logfile ) ) + { + return ; + }else + { + #myprint( "[$logfile]\n" ) ; + my $dirname = dirname( $logfile ) ; + do_valid_directory( $dirname ) || return( 0 ) ; + return( 1 ) ; + } +} + + +sub tests_teelaunch +{ + note( 'Entering tests_teelaunch()' ) ; + + is( undef, teelaunch( ), 'teelaunch: no args => undef' ) ; + my $mysync = {} ; + is( undef, teelaunch( $mysync ), 'teelaunch: arg empty {} => undef' ) ; + $mysync->{logfile} = q{} ; + is( undef, teelaunch( $mysync ), 'teelaunch: logfile empty string => undef' ) ; + + # First time, learning IO::Tee intrasics + $mysync->{logfile} = 'W/tmp/tests/tests_teelaunch.txt' ; + isa_ok( my $tee = teelaunch( $mysync ), 'IO::Tee' , 'teelaunch: logfile W/tmp/tests/tests_teelaunch.txt' ) ; + is( 1, print( $tee "Hi!\n" ), 'teelaunch: write Hi!') ; + is( "Hi!\n", file_to_string( 'W/tmp/tests/tests_teelaunch.txt' ), 'teelaunch: reading W/tmp/tests/tests_teelaunch.txt is Hi!\n' ) ; + is( 1, print( $tee "Hoo\n" ), 'teelaunch: write Hoo') ; + is( "Hi!\nHoo\n", file_to_string( 'W/tmp/tests/tests_teelaunch.txt' ), 'teelaunch: reading W/tmp/tests/tests_teelaunch.txt is Hi!\nHoo\n' ) ; + + # closing so tee won't be happy + close $mysync->{logfile_handle} ; + is( undef, print( $tee "Argh1\n" ), 'teelaunch: write Argh1') ; + is( undef, print( $tee "Argh2\n" ), 'teelaunch: write Argh2') ; + # write not done + is( "Hi!\nHoo\n", file_to_string( 'W/tmp/tests/tests_teelaunch.txt' ), 'teelaunch: reading W/tmp/tests/tests_teelaunch.txt is still Hi!\nHoo\n' ) ; + print join( ' ', $tee->handles ), "\n"; + is( 2, scalar $tee->handles, 'teelaunch: 2 handles') ; + shift @{*{$tee}}; + print join(' ', $tee->handles), "\n" ; + is( 1, scalar $tee->handles, 'teelaunch: 1 handle') ; + is( 1, print( $tee "Argh3\n" ), 'teelaunch: write Argh3 yeah') ; + + shift @{*{$tee}}; + # will not print anything now + is( 0, scalar $tee->handles, 'teelaunch: 0 handle') ; + is( 1, print( $tee "Argh 4\n" ), 'teelaunch: write Argh4 no') ; + + # Second time, lesson learnt IO::Tee + $mysync->{logfile} = 'W/tmp/tests/tests_teelaunch2.txt' ; + isa_ok( $tee = teelaunch( $mysync ), 'IO::Tee' , 'teelaunch: logfile W/tmp/tests/tests_teelaunch2.txt' ) ; + is( 1, print( $tee "Hi!\n" ), 'teelaunch: write Hi!') ; + is( "Hi!\n", file_to_string( 'W/tmp/tests/tests_teelaunch2.txt' ), 'teelaunch: reading W/tmp/tests/tests_teelaunch2.txt is Hi!\n' ) ; + is( 1, print( $tee "Hoo\n" ), 'teelaunch: write Hoo') ; + is( "Hi!\nHoo\n", file_to_string( 'W/tmp/tests/tests_teelaunch2.txt' ), 'teelaunch: reading W/tmp/tests/tests_teelaunch2.txt is Hi!\nHoo\n' ) ; + + is( 1, teefinish( $mysync ), 'teefinish: return 1') ; + is( 1, print( $tee "Argh1\n" ), 'teelaunch: write Argh1') ; + is( 1, print( $tee "Argh2\n" ), 'teelaunch: write Argh2') ; + is( "Hi!\nHoo\n", file_to_string( 'W/tmp/tests/tests_teelaunch2.txt' ), 'teelaunch: reading W/tmp/tests/tests_teelaunch2.txt is still Hi!\nHoo\n' ) ; + is( 1, teefinish( $mysync ), 'teefinish: still return 1') ; + + note( 'Leaving tests_teelaunch()' ) ; + return ; +} + +sub teelaunch +{ + my $mysync = shift ; + + if ( ! defined( $mysync ) ) + { + return ; + } + + my $logfile = $mysync->{logfile} ; + + if ( ! $logfile ) + { + return ; + } + + logfileprepa( $logfile ) || croak "Error no valid directory to write log file $logfile : $OS_ERROR" ; + + # This is a log file opened during the whole sync + ## no critic (InputOutput::RequireBriefOpen) + open my $logfile_handle, '>', $logfile + or croak( "Can not open $logfile for write: $OS_ERROR" ) ; + binmode $logfile_handle, ":encoding(UTF-8)" ; + my $tee = IO::Tee->new( $logfile_handle, \*STDOUT ) ; + $tee->autoflush( 1 ) ; + $mysync->{logfile_handle} = $logfile_handle ; + $mysync->{tee} = $tee ; + return $tee ; +} + +sub teefinish +{ + my $mysync = shift ; + + if ( ! defined( $mysync ) ) { return ; } + + my $tee = $mysync->{tee} ; + + if ( ! defined( $tee ) ) { return ; } + + if ( 2 == scalar $tee->handles ) + { + shift @{*{$tee}}; + } + else + { + # nothing + } + return scalar $tee->handles ; +} + + +sub getpwuid_any_os +{ + my $uid = shift ; + + return( scalar getlogin ) if ( 'MSWin32' eq $OSNAME ) ; # Windows system + return( scalar getpwuid $uid ) ; # Unix system + + +} + + + +sub abortifneeded +{ + my $mysync = shift ; + if ( -e $mysync->{ abortfile } ) + { + myprint( "Asked to terminate by file $mysync->{ abortfile }\n" ) ; + do_and_print_stats( $mysync ) ; + myprint( "You should resynchronize those accounts by running a sync again,\n", + "since some messages and entire folders might still be missing on host2.\n" + ) ; + exit_clean( $mysync, $EXIT_BY_FILE ) ; + return ; + } + else + { + return ; + } +} + +sub simulong +{ + my $mysync = shift ; + + my $max_seconds = $mysync->{ simulong } ; + + if ( ! $max_seconds ) { return ; } + + my $division = 5 ; + my $last_count = int( $division * $max_seconds ) ; + $mysync->{ debug } and myprint "last_count $last_count = int( division $division * max_seconds $max_seconds)\n" ; + foreach my $i ( 1 .. ( $last_count ) ) { + myprint( "Are you still here ETA: " . ( $last_count - $i ) . "/$last_count msgs left\n" ) ; + #this one is for testing huge page behavior + #myprint( "Are you still here ETA: " . ($last_count - $i) . "/$last_count msgs left\n" . ( "Ah" x 40 . "\n") x 4000 ) ; + sleep( 1 / $division ) ; + abortifneeded( $mysync ) ; + } + + return ; +} + + + +sub printenv +{ + myprint( "Environment variables listing:\n", + ( map { "$_ => $ENV{$_}\n" } sort keys %ENV), + "Environment variables listing end\n" ) ; + return ; +} + + +sub unittestssuite +{ + my $mysync = shift ; + if ( ! ( $mysync->{ tests } or $mysync->{ testsdebug } or $mysync->{ testsunit } ) ) { + return ; + } + + my $test_builder = Test::More->builder ; + tests( $mysync ) ; + testsdebug( $mysync ) ; + testunitsession( $mysync ) ; + + my @summary = $test_builder->summary() ; + my @details = $test_builder->details() ; + my $nb_tests_run = scalar( @summary ) ; + my $nb_tests_expected = $test_builder->expected_tests() ; + my $nb_tests_failed = count_0s( @summary ) ; + my $tests_failed = report_failures( @details ) ; + if ( $nb_tests_failed or ( $nb_tests_run != $nb_tests_expected ) ) { + #$test_builder->reset( ) ; + myprint( "Summary of tests: failed $nb_tests_failed tests, run $nb_tests_run tests, expected to run $nb_tests_expected tests.\n", + "List of failed tests:\n", $tests_failed ) ; + return $EXIT_TESTS_FAILED ; + } + + cleanup_mess_from_tests( ) ; + + return 0 ; +} + +sub cleanup_mess_from_tests +{ + undef @pipemess ; + return ; +} + +sub after_get_options +{ + my $mysync = shift ; + my $numopt = shift ; + + + # exit with --help option or no option at all + $mysync->{ debug } and myprint( "numopt:$numopt\n" ) ; + + if ( $help or not $numopt ) { + myprint( usage( $mysync ) ) ; + exit ; + } + + return ; +} + +sub tests_remove_edging_blanks +{ + note( 'Entering tests_remove_edging_blanks()' ) ; + + is( undef, remove_edging_blanks( ), 'remove_edging_blanks: no args => undef' ) ; + is( 'abcd', remove_edging_blanks( 'abcd' ), 'remove_edging_blanks: abcd => abcd' ) ; + is( 'ab cd', remove_edging_blanks( ' ab cd ' ), 'remove_edging_blanks: " ab cd " => "ab cd"' ) ; + + note( 'Leaving tests_remove_edging_blanks()' ) ; + return ; +} + + + +sub remove_edging_blanks +{ + my $string = shift ; + if ( ! defined $string ) + { + return ; + } + $string =~ s,^ +| +$,,g ; + return $string ; +} + + +sub tests_sanitize +{ + note( 'Entering tests_remove_edging_blanks()' ) ; + + is( undef, sanitize( ), 'sanitize: no args => undef' ) ; + my $mysync = {} ; + + $mysync->{ host1 } = ' example.com ' ; + $mysync->{ user1 } = ' to to ' ; + $mysync->{ password1 } = ' sex is good! ' ; + is( undef, sanitize( $mysync ), 'sanitize: => undef' ) ; + is( 'example.com', $mysync->{ host1 }, 'sanitize: host1 " example.com " => "example.com"' ) ; + is( 'to to', $mysync->{ user1 }, 'sanitize: user1 " to to " => "to to"' ) ; + is( 'sex is good!', $mysync->{ password1 }, 'sanitize: password1 " sex is good! " => "sex is good!"' ) ; + note( 'Leaving tests_remove_edging_blanks()' ) ; + return ; +} + + +sub sanitize +{ + my $mysync = shift ; + if ( ! defined $mysync ) + { + return ; + } + + foreach my $parameter ( qw( host1 host2 user1 user2 password1 password2 ) ) + { + $mysync->{ $parameter } = remove_edging_blanks( $mysync->{ $parameter } ) ; + } + return ; +} + +sub easyany +{ + my $mysync = shift ; + + # Gmail + if ( $mysync->{gmail1} and $mysync->{gmail2} ) { + $mysync->{ debug } and myprint( "gmail1 gmail2\n") ; + gmail12( $mysync ) ; + return ; + } + if ( $mysync->{gmail1} ) { + $mysync->{ debug } and myprint( "gmail1\n" ) ; + gmail1( $mysync ) ; + } + if ( $mysync->{gmail2} ) { + $mysync->{ debug } and myprint( "gmail2\n" ) ; + gmail2( $mysync ) ; + } + # Office 365 + if ( $mysync->{office1} ) { + office1( $mysync ) ; + } + + if ( $mysync->{office2} ) { + office2( $mysync ) ; + } + + # Exchange + if ( $mysync->{exchange1} ) { + exchange1( $mysync ) ; + } + + if ( $mysync->{exchange2} ) { + exchange2( $mysync ) ; + } + + + # Domino + if ( $mysync->{domino1} ) { + domino1( $mysync ) ; + } + + if ( $mysync->{domino2} ) { + domino2( $mysync ) ; + } + + return ; +} + +# From and for https://imapsync.lamiral.info/FAQ.d/FAQ.Gmail.txt +sub gmail12 +{ + my $mysync = shift ; + # Gmail at host1 and host2 + $mysync->{host1} ||= 'imap.gmail.com' ; + $mysync->{ssl1} = ( defined $mysync->{ssl1} ) ? $mysync->{ssl1} : 1 ; + $mysync->{host2} ||= 'imap.gmail.com' ; + $mysync->{ssl2} = ( defined $mysync->{ssl2} ) ? $mysync->{ssl2} : 1 ; + $mysync->{maxbytespersecond} ||= 20_000 ; # should be less than 10_000 when computed from Gmail documentation + $mysync->{maxbytesafter} ||= 1_000_000_000 ; # In fact it is documented as half: 500_000_000 + $mysync->{automap} = ( defined $mysync->{automap} ) ? $mysync->{automap} : 1 ; + $mysync->{maxsleep} = ( defined $mysync->{maxsleep} ) ? $mysync->{maxsleep} : $MAX_SLEEP ; ; + $skipcrossduplicates = ( defined $skipcrossduplicates ) ? $skipcrossduplicates : 0 ; + $mysync->{ synclabels } = ( defined $mysync->{ synclabels } ) ? $mysync->{ synclabels } : 1 ; + $mysync->{ resynclabels } = ( defined $mysync->{ resynclabels } ) ? $mysync->{ resynclabels } : 1 ; + push @useheader, 'X-Gmail-Received', 'Message-Id' ; + push @exclude, '\[Gmail\]$' ; + push @folderlast, '[Gmail]/All Mail' ; + return ; +} + + +sub gmail1 +{ + my $mysync = shift ; + # Gmail at host2 + $mysync->{host1} ||= 'imap.gmail.com' ; + $mysync->{ssl1} = ( defined $mysync->{ssl1} ) ? $mysync->{ssl1} : 1 ; + $mysync->{maxbytespersecond} ||= 40_000 ; # should be 30_000 computed from by Gmail documentation + $mysync->{maxbytesafter} ||= 3_000_000_000 ; # + $mysync->{automap} = ( defined $mysync->{automap} ) ? $mysync->{automap} : 1 ; + $mysync->{maxsleep} = ( defined $mysync->{maxsleep} ) ? $mysync->{maxsleep} : $MAX_SLEEP ; ; + $skipcrossduplicates = ( defined $skipcrossduplicates ) ? $skipcrossduplicates : 1 ; + + push @useheader, 'X-Gmail-Received', 'Message-Id' ; + push @{ $mysync->{ regextrans2 } }, 's,\[Gmail\].,,' ; + push @folderlast, '[Gmail]/All Mail' ; + return ; +} + +sub gmail2 +{ + my $mysync = shift ; + # Gmail at host2 + $mysync->{ host2 } ||= 'imap.gmail.com' ; + $mysync->{ ssl2 } = ( defined $mysync->{ ssl2 } ) ? $mysync->{ ssl2 } : 1 ; + $mysync->{ maxbytespersecond } ||= 20_000 ; # should be less than 10_000 computed from by Gmail documentation + $mysync->{ maxbytesafter } ||= 1_000_000_000 ; # In fact it is documented as half: 500_000_000 + + $mysync->{ automap } = ( defined $mysync->{ automap } ) ? $mysync->{ automap } : 1 ; + #$skipcrossduplicates = ( defined $skipcrossduplicates ) ? $skipcrossduplicates : 1 ; + $mysync->{ expunge1 } = ( defined $mysync->{ expunge1 } ) ? $mysync->{ expunge1 } : 1 ; + $mysync->{ addheader } = ( defined $mysync->{ addheader } ) ? $mysync->{ addheader } : 1 ; + $mysync->{ maxsleep } = ( defined $mysync->{ maxsleep } ) ? $mysync->{ maxsleep } : $MAX_SLEEP ; ; + + #$mysync->{ maxsize } = ( defined $mysync->{ maxsize } ) ? $mysync->{ maxsize } : $GMAIL_MAXSIZE ; + + if ( ! $mysync->{ noexclude } ) { + push @exclude, '\[Gmail\]$' ; + } + push @useheader, 'Message-Id' ; + push @{ $mysync->{ regextrans2 } }, 's,\[Gmail\].,,' ; + + # push @{ $mysync->{ regextrans2 } }, 's/[ ]+/_/g' ; # is now replaced + # by the two more specific following regexes, + # they remove just the beginning and trailing blanks, not all. + push @{ $mysync->{ regextrans2 } }, 's,^ +| +$,,g' ; + push @{ $mysync->{ regextrans2 } }, 's,/ +| +/,/,g' ; + # + push @{ $mysync->{ regextrans2 } }, q{s/['\\^"]/_/g} ; # Verified this + push @folderlast, '[Gmail]/All Mail' ; + return ; +} + + +# From https://imapsync.lamiral.info/FAQ.d/FAQ.Exchange.txt +sub office1 +{ + # Office 365 at host1 + my $mysync = shift ; + + output( $mysync, q{Option --office1 is like: --host1 outlook.office365.com --ssl1 --exclude "^Files$"} . "\n" ) ; + output( $mysync, "Option --office1 (cont) : unless overrided with --host1 otherhost --nossl1 --noexclude\n" ) ; + $mysync->{host1} ||= 'outlook.office365.com' ; + $mysync->{ssl1} = ( defined $mysync->{ssl1} ) ? $mysync->{ssl1} : 1 ; + if ( ! $mysync->{noexclude} ) { + push @exclude, '^Files$' ; + } + return ; +} + + +sub office2 +{ + # Office 365 at host2 + my $mysync = shift ; + output( $mysync, qq{Option --office2 is like: --host2 outlook.office365.com --ssl2 --maxsize 45_000_000 --maxmessagespersecond 4\n} ) ; + output( $mysync, qq{Option --office2 (cont) : --disarmreadreceipts --regexmess "wrap 10500" --f1f2 "Files=Files_renamed_by_imapsync"\n} ) ; + output( $mysync, qq{Option --office2 (cont) : unless overrided with --host2 otherhost --nossl2 ... --nodisarmreadreceipts --noregexmess\n} ) ; + output( $mysync, qq{Option --office2 (cont) : and --nof1f2 to avoid Files folder renamed to Files_renamed_by_imapsync\n} ) ; + $mysync->{host2} ||= 'outlook.office365.com' ; + $mysync->{ssl2} = ( defined $mysync->{ssl2} ) ? $mysync->{ssl2} : 1 ; + $mysync->{ maxsize } ||= 45_000_000 ; + $mysync->{maxmessagespersecond} ||= 4 ; + #push @{ $mysync->{ regexflag } }, 's/\\\\Flagged//g' ; # No problem without! tested 2018_09_10 + $disarmreadreceipts = ( defined $disarmreadreceipts ) ? $disarmreadreceipts : 1 ; + # I dislike double negation but here is one + if ( ! $mysync->{noregexmess} ) + { + push @regexmess, 's,(.{10239}),$1\r\n,g' ; + } + # and another... + if ( ! $mysync->{nof1f2} ) + { + push @{ $mysync->{f1f2} }, 'Files=Files_renamed_by_imapsync' ; + } + return ; +} + +sub exchange1 +{ + # Exchange 2010/2013 at host1 + my $mysync = shift ; + output( $mysync, "Option --exchange1 does nothing (except printing this line...)\n" ) ; + # Well nothing to do so far + return ; +} + +sub exchange2 +{ + # Exchange 2010/2013 at host2 + my $mysync = shift ; + output( $mysync, "Option --exchange2 is like: --maxsize 10_000_000 --maxmessagespersecond 4 --disarmreadreceipts\n" ) ; + output( $mysync, "Option --exchange2 (cont) : --regexflag del Flagged --regexmess wrap 10500\n" ) ; + output( $mysync, "Option --exchange2 (cont) : unless overrided with --maxsize xxx --nodisarmreadreceipts --noregexflag --noregexmess\n" ) ; + $mysync->{ maxsize } ||= 10_000_000 ; + $mysync->{maxmessagespersecond} ||= 4 ; + $disarmreadreceipts = ( defined $disarmreadreceipts ) ? $disarmreadreceipts : 1 ; + # I dislike double negation but here are two + if ( ! $mysync->{noregexflag} ) { + push @{ $mysync->{ regexflag } }, 's/\\\\Flagged//g' ; + } + if ( ! $mysync->{noregexmess} ) { + push @regexmess, 's,(.{10239}),$1\r\n,g' ; + } + return ; +} + +sub domino1 +{ + # Domino at host1 + my $mysync = shift ; + + $mysync->{ sep1 } = q{\\} ; + $prefix1 = q{} ; + $messageidnodomain = ( defined $messageidnodomain ) ? $messageidnodomain : 1 ; + return ; +} + +sub domino2 +{ + # Domino at host1 + my $mysync = shift ; + + $mysync->{ sep2 } = q{\\} ; + $prefix2 = q{} ; + $messageidnodomain = ( defined $messageidnodomain ) ? $messageidnodomain : 1 ; + push @{ $mysync->{ regextrans2 } }, 's,^Inbox\\\\(.*),$1,i' ; + return ; +} + + +sub tests_resolv +{ + note( 'Entering tests_resolv()' ) ; + + # is( , resolv( ), 'resolv: => ' ) ; + is( undef, resolv( ), 'resolv: no args => undef' ) ; + is( undef, resolv( q{} ), 'resolv: empty string => undef' ) ; + is( undef, resolv( 'hostnotexist' ), 'resolv: hostnotexist => undef' ) ; + is( '127.0.0.1', resolv( '127.0.0.1' ), 'resolv: 127.0.0.1 => 127.0.0.1' ) ; + is( '127.0.0.1', resolv( 'localhost' ), 'resolv: localhost => 127.0.0.1' ) ; + is( '2001:41d0:2:84e0::1', resolv( 'imapsync.lamiral.info' ), 'resolv: imapsync.lamiral.info => 2001:41d0:2:84e0::1' ) ; + + # ip6-localhost ( in /etc/hosts ) + is( '::1', resolv( 'ip6-localhost' ), 'resolv: ip6-localhost => ::1' ) ; + is( '::1', resolv( '::1' ), 'resolv: ::1 => ::1' ) ; + # ks2ipv6 now has CNAME ks6ipv6 + is( '2001:41d0:8:d8b6::1', resolv( '2001:41d0:8:d8b6::1' ), 'resolv: 2001:41d0:8:d8b6::1 => 2001:41d0:8:d8b6::1' ) ; + is( '2001:41d0:8:9951::1', resolv( 'ks6ipv6.lamiral.info' ), 'resolv: ks6ipv6.lamiral.info => 2001:41d0:8:9951::1' ) ; + # ks6 + is( '2001:41d0:8:9951::1', resolv( '2001:41d0:8:9951::1' ), 'resolv: 2001:41d0:8:9951::1 => 2001:41d0:8:9951::1' ) ; + is( '2001:41d0:8:9951::1', resolv( 'ks6ipv6.lamiral.info' ), 'resolv: ks6ipv6.lamiral.info => 2001:41d0:8:9951::1' ) ; + # ks3 + is( '2001:41d0:8:bebd::1', resolv( '2001:41d0:8:bebd::1' ), 'resolv: 2001:41d0:8:bebd::1 => 2001:41d0:8:bebd::1' ) ; + is( '2001:41d0:8:bebd::1', resolv( 'ks3ipv6.lamiral.info' ), 'resolv: ks3ipv6.lamiral.info => 2001:41d0:8:bebd::1' ) ; + + + note( 'Leaving tests_resolv()' ) ; + return ; +} + + + +sub resolv +{ + my $host = shift @ARG ; + + if ( ! $host ) { return ; } + my $addr ; + if ( defined &Socket::getaddrinfo ) { + $addr = resolv_with_getaddrinfo( $host ) ; + return( $addr ) ; + } + + + + my $iaddr = inet_aton( $host ) ; + if ( ! $iaddr ) { return ; } + $addr = inet_ntoa( $iaddr ) ; + + return $addr ; +} + +sub resolv_with_getaddrinfo +{ + my $host = shift @ARG ; + + $sync->{ debug } and myprint( "Entering resolv_with_getaddrinfo( $host )\n" ) ; + if ( ! $host ) { return ; } + + my ( $err_getaddrinfo, @res ) = Socket::getaddrinfo( $host, "", { socktype => Socket::SOCK_RAW } ) ; + if ( $err_getaddrinfo ) { + myprint( "Cannot getaddrinfo of $host: $err_getaddrinfo\n" ) ; + return ; + } + + my @addr ; + while( my $ai = shift @res ) { + my ( $err_getnameinfo, $ipaddr ) = Socket::getnameinfo( $ai->{addr}, Socket::NI_NUMERICHOST(), Socket::NIx_NOSERV() ) ; + if ( $err_getnameinfo ) { + myprint( "Cannot getnameinfo of $host: $err_getnameinfo\n" ) ; + return ; + }else{ + $sync->{ debug } and myprint( "$host => $ipaddr\n" ) ; + push @addr, $ipaddr ; + my $reverse ; + ( $err_getnameinfo, $reverse ) = Socket::getnameinfo( $ai->{addr}, 0, Socket::NIx_NOSERV() ) ; + $sync->{ debug } and myprint( "$host => $ipaddr => $reverse\n" ) ; + } + $sync->{ debug } and myprint( "$host => $ipaddr\n" ) ; + + } + $sync->{ debug } and myprint( "Leaving resolv_with_getaddrinfo( $host => $addr[0])\n" ) ; + return $addr[0] ; +} + +sub tests_resolvrev +{ + note( 'Entering tests_resolvrev()' ) ; + + # is( , resolvrev( ), 'resolvrev: => ' ) ; + is( undef, resolvrev( ), 'resolvrev: no args => undef' ) ; + is( undef, resolvrev( q{} ), 'resolvrev: empty string => undef' ) ; + is( undef, resolvrev( 'hostnotexist' ), 'resolvrev: hostnotexist => undef' ) ; + is( 'localhost', resolvrev( '127.0.0.1' ), 'resolvrev: 127.0.0.1 => localhost' ) ; + is( 'localhost', resolvrev( 'localhost' ), 'resolvrev: localhost => localhost' ) ; + is( 'ks.lamiral.info', resolvrev( 'imapsync.lamiral.info' ), 'resolvrev: imapsync.lamiral.info => ks.lamiral.info' ) ; + + # ip6-localhost ( in /etc/hosts ) + is( 'ip6-localhost', resolvrev( 'ip6-localhost' ), 'resolvrev: ip6-localhost => ip6-localhost' ) ; + is( 'ip6-localhost', resolvrev( '::1' ), 'resolvrev: ::1 => ip6-localhost' ) ; + # ks2 + is( 'ks6ipv6.lamiral.info', resolvrev( '2001:41d0:8:d8b6::1' ), 'resolvrev: 2001:41d0:8:d8b6::1 => ks6ipv6.lamiral.info' ) ; + is( 'ks6ipv6.lamiral.info', resolvrev( 'ks6ipv6.lamiral.info' ), 'resolvrev: ks6ipv6.lamiral.info => ks6ipv6.lamiral.info' ) ; + # ks3 + is( 'ks3ipv6.lamiral.info', resolvrev( '2001:41d0:8:bebd::1' ), 'resolvrev: 2001:41d0:8:bebd::1 => ks3ipv6.lamiral.info' ) ; + is( 'ks3ipv6.lamiral.info', resolvrev( 'ks3ipv6.lamiral.info' ), 'resolvrev: ks3ipv6.lamiral.info => ks3ipv6.lamiral.info' ) ; + + + note( 'Leaving tests_resolvrev()' ) ; + return ; +} + +sub resolvrev +{ + my $host = shift @ARG ; + + if ( ! $host ) { return ; } + + if ( defined &Socket::getaddrinfo ) { + my $name = resolvrev_with_getaddrinfo( $host ) ; + return( $name ) ; + } + + return ; +} + +sub resolvrev_with_getaddrinfo +{ + my $host = shift @ARG ; + + if ( ! $host ) { return ; } + + my ( $err, @res ) = Socket::getaddrinfo( $host, "", { socktype => Socket::SOCK_RAW } ) ; + if ( $err ) { + myprint( "Cannot getaddrinfo of $host: $err\n" ) ; + return ; + } + + my @name ; + while( my $ai = shift @res ) { + my ( $err, $reverse ) = Socket::getnameinfo( $ai->{addr}, 0, Socket::NIx_NOSERV() ) ; + if ( $err ) { + myprint( "Cannot getnameinfo of $host: $err\n" ) ; + return ; + } + $sync->{ debug } and myprint( "$host => $reverse\n" ) ; + push @name, $reverse ; + } + + return $name[0] ; +} + + + +sub tests_imapsping +{ + note( 'Entering tests_imapsping()' ) ; + + is( undef, imapsping( ), 'imapsping: no args => undef' ) ; + is( undef, imapsping( 'hostnotexist' ), 'imapsping: hostnotexist => undef' ) ; + is( 1, imapsping( 'imapsync.lamiral.info' ), 'imapsping: imapsync.lamiral.info => 1' ) ; + is( 1, imapsping( 'ks6ipv6.lamiral.info' ), 'imapsping: ks6ipv6.lamiral.info => 1' ) ; + note( 'Leaving tests_imapsping()' ) ; + return ; +} + +sub imapsping +{ + my $host = shift ; + return tcpping( $host, $IMAP_SSL_PORT ) ; +} + +sub tests_tcpping +{ + note( 'Entering tests_tcpping()' ) ; + + is( undef, tcpping( ), 'tcpping: no args => undef' ) ; + is( undef, tcpping( 'hostnotexist' ), 'tcpping: one arg => undef' ) ; + is( undef, tcpping( undef, 888 ), 'tcpping: arg undef, port => undef' ) ; + is( undef, tcpping( 'hostnotexist', 993 ), 'tcpping: hostnotexist 993 => undef' ) ; + is( undef, tcpping( 'hostnotexist', 888 ), 'tcpping: hostnotexist 888 => undef' ) ; + is( 1, tcpping( 'imapsync.lamiral.info', 993 ), 'tcpping: imapsync.lamiral.info 993 => 1' ) ; + is( 0, tcpping( 'imapsync.lamiral.info', 888 ), 'tcpping: imapsync.lamiral.info 888 => 0' ) ; + is( 1, tcpping( '5.135.158.182', 993 ), 'tcpping: 5.135.158.182 993 => 1' ) ; + is( 0, tcpping( '5.135.158.182', 888 ), 'tcpping: 5.135.158.182 888 => 0' ) ; + + # Net::Ping supports ipv6 only after release 1.50 + # http://cpansearch.perl.org/src/RURBAN/Net-Ping-2.59/Changes + # Anyway I plan to avoid Net-Ping for that too long standing feature + # Net-Ping is integrated in Perl itself, who knows ipv6 for a long time + is( 1, tcpping( '2001:41d0:8:d8b6::1', 993 ), 'tcpping: 2001:41d0:8:d8b6::1 993 => 1' ) ; + is( 0, tcpping( '2001:41d0:8:d8b6::1', 888 ), 'tcpping: 2001:41d0:8:d8b6::1 888 => 0' ) ; + + note( 'Leaving tests_tcpping()' ) ; + return ; +} + +sub tcpping +{ + if ( 2 != scalar( @ARG ) ) { + return ; + } + my ( $host, $port ) = @ARG ; + if ( ! $host ) { return ; } + if ( ! $port ) { return ; } + + my $mytimeout = $TCP_PING_TIMEOUT ; + require Net::Ping ; + #my $p = Net::Ping->new( 'tcp' ) ; + my $p = Net::Ping->new( ) ; + $p->{port_num} = $port ; + $p->service_check( 1 ) ; + $p->hires( 1 ) ; + my ($ping_ok, $rtt, $ip ) = $p->ping( $host, $mytimeout ) ; + if ( ! defined $ping_ok ) { return ; } + my $rtt_approx = sprintf( "%.3f", $rtt ) ; + $sync->{ debug } and myprint( "Host $host timeout $mytimeout port $port ok $ping_ok ip $ip acked in $rtt_approx s\n" ) ; + $p->close( ) ; + if( $ping_ok ) { + return 1 ; + }else{ + return 0 ; + } +} + +sub tests_sslcheck +{ + note( 'Entering tests_sslcheck()' ) ; + + my $mysync ; + + is( undef, sslcheck( $mysync ), 'sslcheck: no sslcheck => undef' ) ; + + $mysync = { + sslcheck => 1, + } ; + + is( 0, sslcheck( $mysync ), 'sslcheck: no host => 0' ) ; + + $mysync = { + sslcheck => 1, + host1 => 'test1.lamiral.info', + tls1 => 1, + } ; + + is( 0, sslcheck( $mysync ), 'sslcheck: tls1 => 0' ) ; + + $mysync = { + sslcheck => 1, + host1 => 'test1.lamiral.info', + } ; + + + is( 1, sslcheck( $mysync ), 'sslcheck: test1.lamiral.info => 1' ) ; + is( 1, $mysync->{ssl1}, 'sslcheck: test1.lamiral.info => ssl1 1' ) ; + + $mysync->{sslcheck} = 0 ; + is( undef, sslcheck( $mysync ), 'sslcheck: sslcheck off => undef' ) ; + + $mysync = { + sslcheck => 1, + host1 => 'test1.lamiral.info', + host2 => 'test2.lamiral.info', + } ; + + is( 2, sslcheck( $mysync ), 'sslcheck: test1.lamiral.info + test2.lamiral.info => 2' ) ; + + $mysync = { + sslcheck => 1, + host1 => 'test1.lamiral.info', + host2 => 'test2.lamiral.info', + tls1 => 1, + } ; + + is( 1, sslcheck( $mysync ), 'sslcheck: test1.lamiral.info + test2.lamiral.info + tls1 => 1' ) ; + + note( 'Leaving tests_sslcheck()' ) ; + return ; +} + +sub sslcheck +{ + my $mysync = shift ; + + if ( ! $mysync->{sslcheck} ) { + return ; + } + my $nb_on = 0 ; + $mysync->{ debug } and myprint( "sslcheck\n" ) ; + if ( + ( ! defined $mysync->{port1} ) + and + ( ! defined $mysync->{tls1} ) + and + ( ! defined $mysync->{ssl1} ) + and + ( defined $mysync->{host1} ) + ) { + myprint( "Host1: probing ssl on port $IMAP_SSL_PORT ( use --nosslcheck to avoid this ssl probe ) \n" ) ; + if ( probe_imapssl( $mysync->{host1} ) ) { + $mysync->{ssl1} = 1 ; + myprint( "Host1: sslcheck detected open ssl port $IMAP_SSL_PORT so turning ssl on (use --nossl1 --notls1 to turn off SSL and TLS wizardry)\n" ) ; + $nb_on++ ; + }else{ + myprint( "Host1: sslcheck did not detected open ssl port $IMAP_SSL_PORT. Will use standard $IMAP_PORT port.\n" ) ; + } + } + + if ( + ( ! defined $mysync->{port2} ) + and + ( ! defined $mysync->{tls2} ) + and + ( ! defined $mysync->{ssl2} ) + and + ( defined $mysync->{host2} ) + ) { + myprint( "Host2: probing ssl on port $IMAP_SSL_PORT ( use --nosslcheck to avoid this ssl probe ) \n" ) ; + if ( probe_imapssl( $mysync->{host2} ) ) { + $mysync->{ssl2} = 1 ; + myprint( "Host2: sslcheck detected open ssl port $IMAP_SSL_PORT so turning ssl on (use --nossl2 --notls2 to turn off SSL and TLS wizardry)\n" ) ; + $nb_on++ ; + }else{ + myprint( "Host2: sslcheck did not detected open ssl port $IMAP_SSL_PORT. Will use standard $IMAP_PORT port.\n" ) ; + } + } + return $nb_on ; +} + + +sub testslive_init +{ + my $mysync = shift ; + $mysync->{host1} ||= 'test1.lamiral.info' ; + $mysync->{user1} ||= 'test1' ; + $mysync->{password1} ||= 'secret1' ; + $mysync->{host2} ||= 'test2.lamiral.info' ; + $mysync->{user2} ||= 'test2' ; + $mysync->{password2} ||= 'secret2' ; + return ; +} + +sub testslive6_init +{ + my $mysync = shift ; + $mysync->{host1} ||= 'ks6ipv6.lamiral.info' ; + $mysync->{user1} ||= 'test1' ; + $mysync->{password1} ||= 'secret1' ; + $mysync->{host2} ||= 'ks6ipv6.lamiral.info' ; + $mysync->{user2} ||= 'test2' ; + $mysync->{password2} ||= 'secret2' ; + return ; +} + + +sub tests_backslash_caret +{ + note( 'Entering tests_backslash_caret()' ) ; + + is( "lalala", backslash_caret( "lalala" ), 'backslash_caret: lalala => lalala' ) ; + is( "lalala\n", backslash_caret( "lalala\n" ), 'backslash_caret: lalala => lalala 2nd' ) ; + is( '^', backslash_caret( '\\' ), 'backslash_caret: \\ => ^' ) ; + is( "^\n", backslash_caret( "\\\n" ), 'backslash_caret: \\ => ^' ) ; + is( "\\lalala", backslash_caret( "\\lalala" ), 'backslash_caret: \\lalala => \\lalala' ) ; + is( "\\lal\\ala", backslash_caret( "\\lal\\ala" ), 'backslash_caret: \\lal\\ala => \\lal\\ala' ) ; + is( "\\lalala\n", backslash_caret( "\\lalala\n" ), 'backslash_caret: \\lalala => \\lalala 2nd' ) ; + is( "lalala^\n", backslash_caret( "lalala\\\n" ), 'backslash_caret: lalala\\\n => lalala^\n' ) ; + is( "lalala^\nlalala^\n", backslash_caret( "lalala\\\nlalala\\\n" ), 'backslash_caret: lalala\\\nlalala\\\n => lalala^\nlalala^\n' ) ; + is( "lal\\ala^\nlalala^\n", backslash_caret( "lal\\ala\\\nlalala\\\n" ), 'backslash_caret: lal\\ala\\\nlalala\\\n => lal\\ala^\nlalala^\n' ) ; + + note( 'Leaving tests_backslash_caret()' ) ; + return ; +} + +sub backslash_caret +{ + my $string = shift ; + + $string =~ s{\\ $ }{^}gxms ; + + return $string ; +} + +sub tests_split_around_equal +{ + note( 'Entering tests_split_around_equal()' ) ; + + is( undef, split_around_equal( ), 'split_around_equal: no args => undef' ) ; + is_deeply( { toto => 'titi' }, { split_around_equal( 'toto=titi' ) }, 'split_around_equal: toto=titi => toto => titi' ) ; + is_deeply( { A => 'B', C => 'D' }, { split_around_equal( 'A=B=C=D' ) }, 'split_around_equal: toto=titi => toto => titi' ) ; + is_deeply( { A => 'B', C => 'D' }, { split_around_equal( 'A=B', 'C=D' ) }, 'split_around_equal: A=B C=D => A => B, C=>D' ) ; + + note( 'Leaving tests_split_around_equal()' ) ; + return ; +} + +sub split_around_equal +{ + if ( ! @ARG ) { return ; } ; + return map { split /=/mxs, $_ } @ARG ; + +} + + + +sub tests_sig_install +{ + note( 'Entering tests_sig_install()' ) ; + + my $mysync ; + is( undef, sig_install( ), 'sig_install: no args => undef' ) ; + is( undef, sig_install( $mysync ), 'sig_install: arg undef => undef' ) ; + $mysync = { } ; + is( undef, sig_install( $mysync ), 'sig_install: empty hash => undef' ) ; + + SKIP: { + Readonly my $SKIP_15 => 15 ; + if ( 'MSWin32' eq $OSNAME ) { skip( 'Tests only for Unix', $SKIP_15 ) ; } + # Default to ignore USR1 USR2 in case future install fails + local $SIG{ USR1 } = local $SIG{ USR2 } = sub { } ; + kill( 'USR1', $PROCESS_ID ) ; + + $mysync->{ debugsig } = 1 ; + # Assign USR1 to call sub tototo + # Surely a better value than undef should be returned when doing real signal stuff + is( undef, sig_install( $mysync, 'tototo', 'USR1' ), 'sig_install: USR1 tototo' ) ; + + is( 1, kill( 'USR1', $PROCESS_ID ), 'sig_install: kill USR1 myself 1' ) ; + is( 1, $mysync->{ tototo_calls }, 'sig_install: tototo call nb 1' ) ; + + #return ; + # Assign USR2 to call sub tototo + is( undef, sig_install( $mysync, 'tototo', 'USR2' ), 'sig_install: USR2 tototo' ) ; + + is( 1, kill( 'USR2', $PROCESS_ID ), 'sig_install: kill USR2 myself 1' ) ; + is( 2, $mysync->{ tototo_calls }, 'sig_install: tototo call nb 2' ) ; + + is( 1, kill( 'USR1', $PROCESS_ID ), 'sig_install: kill USR1 myself 2' ) ; + is( 3, $mysync->{ tototo_calls }, 'sig_install: tototo call nb 3' ) ; + + + local $SIG{ USR1 } = local $SIG{ USR2 } = sub { } ; + is( 1, kill( 'USR1', $PROCESS_ID ), 'sig_install: kill USR1 myself 3' ) ; + is( 3, $mysync->{ tototo_calls }, 'sig_install: tototo call still nb 3' ) ; + + # Assign USR1 + USR2 to call sub tototo + is( undef, sig_install( $mysync, 'tototo', 'USR1', 'USR2' ), 'sig_install: USR1 USR2 tototo' ) ; + is( 1, kill( 'USR1', $PROCESS_ID ), 'sig_install: kill USR1 myself 4' ) ; + is( 4, $mysync->{ tototo_calls }, 'sig_install: tototo call now nb 4' ) ; + + is( 1, kill( 'USR2', $PROCESS_ID ), 'sig_install: kill USR1 myself 2' ) ; + is( 5, $mysync->{ tototo_calls }, 'sig_install: tototo call now nb 5' ) ; + } + + + note( 'Leaving tests_sig_install()' ) ; + return ; +} + + +# +sub sig_install +{ + my $mysync = shift ; + if ( ! $mysync ) { return ; } + my $mysubname = shift ; + if ( ! $mysubname ) { return ; } + + if ( ! @ARG ) { return ; } + + my @signals = @ARG ; + + my $mysub = \&$mysubname ; + #$mysync->{ debugsig } = 1 ; + $mysync->{ debugsig } and myprint( "In sig_install with sub $mysubname and signal @ARG\n" ) ; + + my $subsignal = sub { + my $signame = shift ; + $mysync->{ debugsig } and myprint( "In subsignal with $signame and $mysubname\n" ) ; + &$mysub( $mysync, $signame ) ; + } ; + + foreach my $signal ( @signals ) { + $mysync->{ debugsig } and myprint( "Installing signal $signal to call sub $mysubname\n") ; + output( $mysync, "kill -$signal $PROCESS_ID # special behavior: call to sub $mysubname\n" ) ; + ## no critic (RequireLocalizedPunctuationVars) + $SIG{ $signal } = $subsignal ; + } + return ; +} + + +sub tototo +{ + my $mysync = shift ; + myprint("In tototo with @ARG\n" ) ; + $mysync->{ tototo_calls } += 1 ; + return ; +} + +sub mygetppid +{ + if ( 'MSWin32' eq $OSNAME ) { + return( 'unknown under MSWin32 (too complicated)' ) ; + } else { + # Unix + return( getppid( ) ) ; + } +} + + +sub tests_toggle_sleep +{ + note( 'Entering tests_toggle_sleep()' ) ; + + is( undef, toggle_sleep( ), 'toggle_sleep: no args => undef' ) ; + my $mysync ; + is( undef, toggle_sleep( $mysync ), 'toggle_sleep: undef => undef' ) ; + $mysync = { } ; + is( undef, toggle_sleep( $mysync ), 'toggle_sleep: no maxsleep => undef' ) ; + + $mysync->{maxsleep} = 3 ; + is( 0, toggle_sleep( $mysync ), 'toggle_sleep: 3 => 0' ) ; + + is( $MAX_SLEEP, toggle_sleep( $mysync ), "toggle_sleep: 0 => $MAX_SLEEP" ) ; + is( 0, toggle_sleep( $mysync ), "toggle_sleep: $MAX_SLEEP => 0" ) ; + is( $MAX_SLEEP, toggle_sleep( $mysync ), "toggle_sleep: 0 => $MAX_SLEEP" ) ; + is( 0, toggle_sleep( $mysync ), "toggle_sleep: $MAX_SLEEP => 0" ) ; + + SKIP: { + Readonly my $SKIP_9 => 9 ; + if ( 'MSWin32' eq $OSNAME ) { skip( 'Tests only for Unix', $SKIP_9 ) ; } + # Default to ignore USR1 USR2 in case future install fails + local $SIG{ USR1 } = sub { } ; + kill( 'USR1', $PROCESS_ID ) ; + + $mysync->{ debugsig } = 1 ; + # Assign USR1 to call sub toggle_sleep + is( undef, sig_install( $mysync, \&toggle_sleep, 'USR1' ), 'toggle_sleep: install USR1 toggle_sleep' ) ; + + + $mysync->{maxsleep} = 4 ; + is( 1, kill( 'USR1', $PROCESS_ID ), 'toggle_sleep: kill USR1 myself' ) ; + is( 0, $mysync->{ maxsleep }, 'toggle_sleep: toggle_sleep called => sleeps are 0s' ) ; + + is( 1, kill( 'USR1', $PROCESS_ID ), 'toggle_sleep: kill USR1 myself again' ) ; + is( $MAX_SLEEP, $mysync->{ maxsleep }, "toggle_sleep: toggle_sleep called => sleeps are ${MAX_SLEEP}s" ) ; + + is( 1, kill( 'USR1', $PROCESS_ID ), 'toggle_sleep: kill USR1 myself' ) ; + is( 0, $mysync->{ maxsleep }, 'toggle_sleep: toggle_sleep called => sleeps are 0s' ) ; + + is( 1, kill( 'USR1', $PROCESS_ID ), 'toggle_sleep: kill USR1 myself again' ) ; + is( $MAX_SLEEP, $mysync->{ maxsleep }, "toggle_sleep: toggle_sleep called => sleeps are ${MAX_SLEEP}s" ) ; + } + + note( 'Leaving tests_toggle_sleep()' ) ; + return ; +} + + +sub toggle_sleep +{ + my $mysync = shift ; + + myprint("In toggle_sleep with @ARG\n" ) ; + + if ( !defined( $mysync ) ) { return ; } + if ( !defined( $mysync->{maxsleep} ) ) { return ; } + + $mysync->{ maxsleep } = max( 0, $MAX_SLEEP - $mysync->{maxsleep} ) ; + myprint("Resetting maxsleep to ", $mysync->{maxsleep}, "s\n" ) ; + return $mysync->{maxsleep} ; +} + +sub mypod2usage +{ + my $fh_pod2usage = shift ; + + pod2usage( + -exitval => 'NOEXIT', + -noperldoc => 1, + -verbose => 99, + -sections => [ qw(NAME VERSION USAGE OPTIONS) ], + -indent => 1, + -loose => 1, + -output => $fh_pod2usage, + ) ; + + return ; +} + +sub usage +{ + my $mysync = shift ; + + if ( ! defined $mysync ) { return ; } + + my $usage = q{} ; + my $usage_from_pod ; + my $usage_footer = usage_footer( $mysync ) ; + + # pod2usage writes on a filehandle only and I want a variable + open my $fh_pod2usage, ">", \$usage_from_pod + or do { warn $OS_ERROR ; return ; } ; + mypod2usage( $fh_pod2usage ) ; + close $fh_pod2usage ; + + if ( 'MSWin32' eq $OSNAME ) { + $usage_from_pod = backslash_caret( $usage_from_pod ) ; + } + $usage = join( q{}, $usage_from_pod, $usage_footer ) ; + + return( $usage ) ; +} + +sub tests_usage +{ + note( 'Entering tests_usage()' ) ; + + my $usage ; + like( $usage = usage( $sync ), qr/Name:/, 'usage: contains Name:' ) ; + myprint( $usage ) ; + like( $usage, qr/Version:/, 'usage: contains Version:' ) ; + like( $usage, qr/Usage:/, 'usage: contains Usage:' ) ; + like( $usage, qr/imapsync/, 'usage: contains imapsync' ) ; + + is( undef, usage( ), 'usage: no args => undef' ) ; + + note( 'Leaving tests_usage()' ) ; + return ; +} + + +sub usage_footer +{ + my $mysync = shift ; + + my $footer = q{} ; + + my $localhost_info = localhost_info( $mysync ) ; + my $rcs = $mysync->{rcs} ; + my $homepage = homepage( ) ; + + my $imapsync_release = $STR_use_releasecheck ; + + if ( $mysync->{releasecheck} ) { + $imapsync_release = check_last_release( ) ; + } + + $footer = qq{$localhost_info +$rcs +$imapsync_release +$homepage +} ; + return( $footer ) ; +} + + + +sub usage_complete +{ + # Unused, I guess this function could be deleted + my $usage = <<'EOF' ; +--skipheader reg : Don't take into account header keyword + matching reg ex: --skipheader 'X.*' + +--skipsize : Don't take message size into account to compare + messages on both sides. On by default. + Use --no-skipsize for using size comparaison. +--allowsizemismatch : allow RFC822.SIZE != fetched msg size + consider also --skipsize to avoid duplicate messages + when running syncs more than one time per mailbox + +--reconnectretry1 int : reconnect to host1 if connection is lost up to + int times per imap command (default is 3) +--reconnectretry2 int : same as --reconnectretry1 but for host2 +--split1 int : split the requests in several parts on host1. + int is the number of messages handled per request. + default is like --split1 100. +--split2 int : same thing on host2. +--nofixInboxINBOX : Don't fix Inbox INBOX mapping. +EOF + return( $usage ) ; +} + + + + +sub setvalfromcgikey +{ + my ( $mysync, $mycgi, $key, $val ) = @ARG ; + + my $badthings = 0 ; + + + my ( $name, $type, $struct ) ; + if ( $key !~ m/^([\w\d\|]+)([=:][isf])?([\+!\@\%])?$/mxs ) + { + $badthings++ ; + next ; # Unknown item + } + else + { + $name = [ split '|', $1, 1 ]->[0] ; # option name ab|cd|ef => keep only ab + $type = $2 ; # = or : followed by i or s or f + $struct = $3 ; # + or ! or @ or % + } + + if ( ( $struct || q{} ) eq '+' ) + { + ${$val} = $mycgi->param( $name ) ; # "Incremental" integer + } + elsif ( $type ) + { + my @values = $mycgi->multi_param( $name ) ; + + #myprint( "type[$type]values[@values]\$struct[", $struct || q{}, "]val[$val]ref(val)[", ref($val), "]\n" ) ; + if ( ( $struct || q{} ) eq '%' or ref( $val ) eq 'HASH' ) + { + setvalfromhash( $val, $type, @values ) ; + } + else + { + setvalfromlist( $mysync, $val, $name, $type, $struct, @values ) ; + } + } + else + { + setvalfromcheckbox( $mysync, $mycgi, $key, $name, $val ) ; + } + + return $badthings ; +} + +sub setvalfromlist +{ + my ( $mysync, $val, $name, $type, $struct, @values ) = @ARG ; + if ( $type =~ m/i$/mxs ) + { + @values = map { q{} ne $_ ? int $_ : undef } @values ; + } + elsif ( $type =~ m/f$/mxs ) + { + @values = map { 0 + $_ } @values ; + } + + if ( ( $struct || q{} ) eq '@' ) + { + @{ ${$val} } = @values ; + my @option = map { +( "--$name", "$_" ) } @values ; + push @{ $mysync->{ cmdcgi } }, @option ; + } + elsif ( ref( $val ) eq 'ARRAY' ) + { + @{$val} = @values ; + } + elsif ( my $value = $values[0] ) + { + ${$val} = $value ; + push @{ $mysync->{ cmdcgi } }, "--$name", $value ; + } + else + { + } + + return ; +} +sub setvalfromhash +{ + my ( $val, $type, @values ) = @ARG ; + + my %values = map { split /=/mxs, $_ } @values ; + + if ( $type =~ m/i$/mxs ) + { + foreach my $k ( keys %values ) + { + $values{$k} = int $values{$k} ; + } + } + elsif ( $type =~ m/f$/mxs ) + { + foreach my $k ( keys %values ) { + $values{$k} = 0 + $values{$k}; + } + } + + if ( 'REF' eq ref $val ) + { + %{ ${$val} } = %values ; + } + else + { + %{$val} = %values ; + } + + return ; +} + + +sub setvalfromcheckbox +{ + my ( $mysync, $mycgi, $key, $name, $val ) = @ARG ; + + # Checkbox + # --noname is set by name=0 or name= + my $value = $mycgi->param( $name ) ; + if ( defined $value ) + { + ${$val} = $value ; + if ( $value ) + { + push @{ $mysync->{ cmdcgi } }, "--$name" ; + } + else + { + push @{ $mysync->{ cmdcgi } }, "--no$name" ; + } + } + else + { + ${$val} = undef ; + } + return ; +} + +sub myGetOptions +{ + + # Started as a copy of Luke Ross Getopt::Long::CGI + # https://metacpan.org/release/Getopt-Long-CGI + # So this sub function is under the same license as Getopt-Long-CGI Luke Ross wants it, + # which was Perl 5.6 or later licenses at the date of the copy. + # It also applies for the sub functions called from this one. + + my $mysync = shift @ARG ; + my $arguments_ref = shift @ARG ; + my %options = @ARG ; + + my $mycgi = $mysync->{cgi} ; + + if ( not under_cgi_context() ) { + + # Not CGI - pass upstream for normal command line handling + return Getopt::Long::GetOptionsFromArray( $arguments_ref, %options ) ; + } + + # We must be in CGI context now + if ( ! defined( $mycgi ) ) { return ; } + + my $badthings = 0 ; + foreach my $key ( sort keys %options ) { + my $val = $options{$key} ; + + $badthings += setvalfromcgikey( $mysync, $mycgi, $key, $val ) ; + + } + + if ( $badthings ) { + return ; # undef or () + } + else { + return ( 1 ) ; + } +} + + + +sub tests_get_options_extra +{ + note( 'Entering tests_get_options_extra()' ) ; + + is( undef, get_options_extra( ), 'get_options_extra: no args => undef' ) ; + + my $mysync = { } ; + is( undef, get_options_extra( $mysync ), 'get_options_extra: undef => undef' ) ; + + my $cwd_save = getcwd( ) ; + + ok( (-d 'W/tmp/tests/options_extra/' or mkpath( 'W/tmp/tests/options_extra/' )), 'get_options_extra: mkpath W/tmp/tests/options_extra/' ) ; + + chdir 'W/tmp/tests/options_extra/' ; + + is( '--debugimap1', string_to_file( '--debugimap1', 'options_extra.txt' ), 'get_options_extra: string_to_file filling options_extra.txt with --debugimap1' ) ; + + is( '--debugimap1', file_to_string( 'options_extra.txt' ), 'get_options_extra: reading options_extra.txt is --debugimap1' ) ; + + is( '', get_options_extra( $mysync ), 'get_options_extra: --debugimap1 in options_extra.txt => nothing left, empty string return' ) ; + + is( 1, $mysync->{ acc1 }->{ debugimap }, 'get_options_extra: --debugimap1 in options_extra.txt => ok, acc1->debugimap = 1' ) ; + + is( '--tls1 proutcaca', string_to_file( '--tls1 proutcaca', 'options_extra.txt' ), 'get_options_extra: string_to_file filling options_extra.txt with --tls1 proutcaca' ) ; + + is( 'proutcaca', get_options_extra( $mysync ), 'get_options_extra: --tls1 proutcaca in options_extra.txt => proutcaca left, proutcaca return' ) ; + + chdir $cwd_save ; + + note( 'Leaving tests_get_options_extra()' ) ; + return ; +} + +sub get_options_extra +{ + my $mysync = shift @ARG ; + + if ( ! defined $mysync ) { return ; } + + if ( -f -r 'options_extra.txt' ) + { + my $cwd = getcwd( ) ; + my $string = firstline( 'options_extra.txt' ) ; + my $rest = get_options_from_string( $mysync, $string ) ; + output( $mysync, "Reading extra options from file options_extra.txt (cwd: $cwd) : $string\n" ) ; + return $rest ; + } + else + { + return ; + } +} + + +sub tests_get_options_from_string +{ + note( 'Entering tests_get_options_from_string()' ) ; + + is( undef, get_options_from_string( ), 'get_options_from_string: no args => undef' ) ; + my $mysync = { } ; + is( undef, get_options_from_string( $mysync ), 'get_options_from_string: undef => undef' ) ; + + is( '', get_options_from_string( $mysync, '--debugimap1' ), + 'get_options_from_string: --debugimap1 => ok, nothing left, empty string return' ) ; + is( 1, $mysync->{ acc1 }->{ debugimap }, 'get_options_from_string: --debugimap1 => ok, acc1->debugimap = 1' ) ; + + $mysync = { } ; # reset + is( 'caca', get_options_from_string( $mysync, '--debugimap1 caca' ), + 'get_options_from_string: --debugimap1 caca => ok, caca left, caca return' ) ; + is( 1, $mysync->{ acc1 }->{ debugimap }, 'get_options_from_string: --debugimap1 => ok, acc1->debugimap = 1' ) ; + + is( 'popo roro', get_options_from_string( $mysync, '--debugimap2 popo roro' ), + 'get_options_from_string: --debugimap1 popo roro => ok, popo roro left, popo roro return' ) ; + is( 1, $mysync->{ acc2 }->{ debugimap }, 'get_options_from_string: --debugimap2 popo roro => ok, acc2->debugimap = 1' ) ; + is( 1, $mysync->{ acc1 }->{ debugimap }, 'get_options_from_string: acc1->debugimap = 1 still' ) ; + + is( '', get_options_from_string( $mysync, '--nodebugimap1 --debugflags --errorsmax 2' ), + 'get_options_from_string: --nodebugimap1 --debugflags --errorsmax 2 => ok, empty string return' ) ; + + is( 0, $mysync->{ acc1 }->{ debugimap }, 'get_options_from_string: acc1->debugimap = 0 now' ) ; + is( 1, $debugflags, 'get_options_from_string: debugflags = 1 now' ) ; + is( 2, $mysync->{ errorsmax }, 'get_options_from_string: mysync->errorsmax = 2 now' ) ; + + is( '', get_options_from_string( $mysync, '--folder "IN BOX" --folder JOE' ), + 'get_options_from_string: --folder "IN BOX" --folder JOE => ok, empty string return' ) ; + + is_deeply( [ 'IN BOX', 'JOE' ], [@{$mysync->{ folder }}], 'get_options_from_string: "IN BOX" "JOE"' ) ; + + is( '', get_options_from_string( $mysync, '--debugflags --koko' ), + 'get_options_from_string: --debugflags --koko => ok, empty string return, with "Unknown option: koko" on STDERR' ) ; + + note( 'Leaving tests_get_options_from_string()' ) ; + return ; +} + +sub get_options_from_string +{ + my $mysync = shift @ARG ; + my $mystring = shift @ARG ; + + if ( ! defined $mystring ) { return ; } + + my ( $ret, $args ) = Getopt::Long::GetOptionsFromString( $mystring, + 'debugimap!' => \$mysync->{ debugimap }, + 'debugimap1!' => \$mysync->{ acc1 }->{ debugimap }, + 'debugimap2!' => \$mysync->{ acc2 }->{ debugimap }, + 'debugflags!' => \$debugflags, + 'debugsleep=f' => \$mysync->{ debugsleep }, + 'errorsmax=i' => \$mysync->{ errorsmax }, + 'folder=s@' => \$mysync->{ folder }, + 'timeout=f' => \$mysync->{ timeout }, + 'timeout1=f' => \$mysync->{ acc1 }->{ timeout }, + 'timeout2=f' => \$mysync->{ acc2 }->{ timeout }, + 'keepalive1!' => \$mysync->{ acc1 }->{ keepalive }, + 'keepalive2!' => \$mysync->{ acc2 }->{ keepalive }, + 'reconnectretry1=i' => \$mysync->{ acc1 }->{ reconnectretry }, + 'reconnectretry2=i' => \$mysync->{ acc2 }->{ reconnectretry }, + 'ssl1!' => \$mysync->{ ssl1 }, + 'ssl2!' => \$mysync->{ ssl2 }, + 'tls1!' => \$mysync->{ tls1 }, + 'tls2!' => \$mysync->{ tls2 }, + 'compress1!' => \$mysync->{ acc1 }->{ compress }, + 'compress2!' => \$mysync->{ acc2 }->{ compress }, + ) ; + my $left = join( ' ', @$args ) ; + return $left ; +} + + + + + + +sub tests_get_options_cgi_context +{ + note( 'Entering tests_get_options_cgi_context()' ) ; + + +# Temporary, have to think harder about testing CGI context in command line --tests + # API: + # * input arguments: two ways, command line or CGI + # * the program arguments + # * QUERY_STRING env variable + # * return + # * QUERY_STRING length + + # CGI context + local $ENV{SERVER_SOFTWARE} = 'Votre serviteur' ; + + # Real full test + # = 'host1=test1.lamiral.info&user1=test1&password1=secret1&host2=test2.lamiral.info&user2=test2&password2=secret2&debugenv=on' + my $mysync ; + is( undef, get_options( $mysync ), 'get_options cgi context: no CGI module => undef' ) ; + + # skip all next tests if the CGI module is not available + + SKIP: { + if ( ! eval { require CGI ; } ) { + skip( "CGI Perl module is not installed", 19 ) ; + } + + CGI->import( qw( -no_debug -utf8 ) ) ; + + is( undef, get_options( $mysync ), 'get_options cgi context: no CGI param => undef' ) ; + # Testing boolean + $mysync->{cgi} = CGI->new( 'version=on&debugenv=on' ) ; + local $ENV{'QUERY_STRING'} = 'version=on&debugenv=on' ; + is( 22, get_options( $mysync ), 'get_options cgi context: QUERY_STRING => 22' ) ; + is( 'on', $mysync->{ version }, 'get_options cgi context: --version => on' ) ; + # debugenv is not allowed in cgi context + is( undef, $mysync->{debugenv}, 'get_options cgi context: $mysync->{debugenv} => undef' ) ; + + # QUERY_STRING in this test is only for return value of get_options + # Have to think harder, GET/POST context, is this return value a good thing? + local $ENV{'QUERY_STRING'} = 'host1=test1.lamiral.info&user1=test1' ; + $mysync->{cgi} = CGI->new( 'host1=test1.lamiral.info&user1=test1' ) ; + is( 36, get_options( $mysync, ), 'get_options cgi context: QUERY_STRING => 36' ) ; + is( 'test1', $mysync->{user1}, 'get_options cgi context: $mysync->{user1} => test1' ) ; + #local $ENV{'QUERY_STRING'} = undef ; + + # Testing s@ as ref + $mysync->{cgi} = CGI->new( 'folder=fd1' ) ; + get_options( $mysync ) ; + is_deeply( [ 'fd1' ], $mysync->{ folder }, 'get_options cgi context: $mysync->{ folder } => fd1' ) ; + $mysync->{cgi} = CGI->new( 'folder=fd1&folder=fd2' ) ; + get_options( $mysync ) ; + is_deeply( [ 'fd1', 'fd2' ], $mysync->{ folder }, 'get_options cgi context: $mysync->{ folder } => fd1, fd2' ) ; + + # Testing % + $mysync->{cgi} = CGI->new( 'f1f2h=s1=d1&f1f2h=s2=d2&f1f2h=s3=d3' ) ; + get_options( $mysync ) ; + + is_deeply( { 's1' => 'd1', 's2' => 'd2', 's3' => 'd3' }, + $mysync->{f1f2h}, 'get_options cgi context: f1f2h => s1=d1 s2=d2 s3=d3' ) ; + + # Testing boolean ! with --noxxx, doesnot work + $mysync->{cgi} = CGI->new( 'nodry=on' ) ; + get_options( $mysync ) ; + is( undef, $mysync->{dry}, 'get_options cgi context: --nodry => $mysync->{dry} => undef' ) ; + + $mysync->{cgi} = CGI->new( 'host1=example.com' ) ; + get_options( $mysync ) ; + is( 'example.com', $mysync->{host1}, 'get_options cgi context: --host1=example.com => $mysync->{host1} => example.com' ) ; + + #myprint( Data::Dumper->Dump( [ $mysync ] ) ) ; + $mysync->{cgi} = CGI->new( 'simulong=' ) ; + get_options( $mysync ) ; + is( undef, $mysync->{simulong}, 'get_options cgi context: --simulong= => $mysync->{simulong} => undef' ) ; + + $mysync->{cgi} = CGI->new( 'simulong' ) ; + get_options( $mysync ) ; + is( undef, $mysync->{simulong}, 'get_options cgi context: --simulong => $mysync->{simulong} => undef' ) ; + + $mysync->{cgi} = CGI->new( 'simulong=4' ) ; + get_options( $mysync ) ; + is( 4, $mysync->{simulong}, 'get_options cgi context: --simulong=4 => $mysync->{simulong} => 4' ) ; + is( undef, $mysync->{ folder }, 'get_options cgi context: --simulong=4 => $mysync->{ folder } => undef' ) ; + #myprint( Data::Dumper->Dump( [ $mysync ] ) ) ; + + $mysync ={} ; + $mysync->{cgi} = CGI->new( 'testslive=on' ) ; + get_options( $mysync ) ; + is( 'on', $mysync->{ testslive }, 'get_options cgi context: --testslive=on => testslive => on' ) ; + #myprint( Data::Dumper->Dump( [ $mysync ] ) ) ; + + $mysync ={} ; + $mysync->{cgi} = CGI->new( 'log=0' ) ; + get_options( $mysync ) ; + is( 0, $mysync->{ log }, 'get_options cgi context: --log=0 => log => 0' ) ; + #myprint( Data::Dumper->Dump( [ $mysync ] ) ) ; + + + # What is this fucked up indentation? + } + + + note( 'Leaving tests_get_options_cgi_context()' ) ; + return ; +} + + + +sub get_options_cgi +{ + # In CGI context arguments are not in @ARGV but in QUERY_STRING variable (with GET). + my $mysync = shift @ARG ; + $mysync->{cgi} || return ; + my @arguments = @ARG ; + # final 0 is used to print usage when no option is given + my $numopt = length $ENV{'QUERY_STRING'} || 1 ; + $mysync->{f1f2h} = {} ; + my $opt_ret = myGetOptions( + $mysync, + \@arguments, + 'abort' => \$mysync->{ abort }, + 'abortbyfile' => \$mysync->{ abortbyfile }, + 'host1=s' => \$mysync->{ host1 }, + 'host2=s' => \$mysync->{ host2 }, + 'user1=s' => \$mysync->{ user1 }, + 'user2=s' => \$mysync->{ user2 }, + 'password1=s' => \$mysync->{ password1 }, + 'password2=s' => \$mysync->{ password2 }, + 'dry!' => \$mysync->{ dry }, + 'dry1!' => \$mysync->{ dry1 }, + 'version' => \$mysync->{ version }, + 'ssl1!' => \$mysync->{ ssl1 }, + 'ssl2!' => \$mysync->{ ssl2 }, + 'tls1!' => \$mysync->{ tls1 }, + 'tls2!' => \$mysync->{ tls2 }, + 'compress1!' => \$mysync->{ acc1 }->{ compress }, + 'compress2!' => \$mysync->{ acc2 }->{ compress }, + 'justbanner!' => \$mysync->{ justbanner }, + 'justlogin!' => \$mysync->{ justlogin }, + 'justconnect!' => \$mysync->{ justconnect }, + 'addheader!' => \$mysync->{ addheader }, + 'automap!' => \$mysync->{ automap }, + 'justautomap!' => \$mysync->{ justautomap }, + 'gmail1' => \$mysync->{ gmail1 }, + 'gmail2' => \$mysync->{ gmail2 }, + 'office1' => \$mysync->{ office1 }, + 'office2' => \$mysync->{ office2 }, + 'exchange1' => \$mysync->{ exchange1 }, + 'exchange2' => \$mysync->{ exchange2 }, + 'domino1' => \$mysync->{ domino1 }, + 'domino2' => \$mysync->{ domino2 }, + 'f1f2=s@' => \$mysync->{ f1f2 }, + 'f1f2h=s%' => \$mysync->{ f1f2h }, + 'folder=s@' => \$mysync->{ folder }, + 'testslive!' => \$mysync->{ testslive }, + 'testslive6!' => \$mysync->{ testslive6 }, + 'releasecheck!' => \$mysync->{ releasecheck }, + 'simulong=f' => \$mysync->{ simulong }, + 'debugsleep=f' => \$mysync->{ debugsleep }, + 'subfolder1=s' => \$mysync->{ subfolder1 }, + 'subfolder2=s' => \$mysync->{ subfolder2 }, + 'justfolders!' => \$mysync->{ justfolders }, + 'justfoldersizes!' => \$mysync->{ justfoldersizes }, + 'delete1!' => \$mysync->{ delete1 }, + 'delete2!' => \$mysync->{ delete2 }, + 'delete2duplicates!' => \$mysync->{ delete2duplicates }, + 'tail!' => \$mysync->{ tail }, + 'tmphash=s' => \$mysync->{ tmphash }, + 'exitwhenover=i' => \$mysync->{ exitwhenover }, + 'syncduplicates!' => \$mysync->{ syncduplicates }, + 'log!' => \$mysync->{ log }, + 'loglogfile!' => \$mysync->{ loglogfile }, + + +# f1f2h=s% could be removed but +# tests_get_options_cgi() should be split before +# with a sub tests_myGetOptions() + ) ; + + $mysync->{ debug } and output( $mysync, "get options: [$opt_ret][$numopt]\n" ) ; + + if ( ! $opt_ret ) { + return ; + } + return $numopt ; +} + +sub get_options_cmd +{ + my $mysync = shift @ARG ; + my @arguments = @ARG ; + my $mycgi = $mysync->{cgi} ; + # final 0 is used to print usage when no option is given on command line + my $numopt = scalar @arguments || 0 ; + my $argv = join "\x00", @arguments ; + + if ( $argv =~ m/-delete\x002/x ) { + output( $mysync, "May be you mean --delete2 instead of --delete 2\n" ) ; + return ; + } + $mysync->{f1f2h} = {} ; + my $opt_ret = myGetOptions( + $mysync, + \@arguments, + 'debug!' => \$mysync->{ debug }, + 'debuglist!' => \$debuglist, + 'debugcontent!' => \$mysync->{ debugcontent }, + 'debugsleep=f' => \$mysync->{ debugsleep }, + 'debugflags!' => \$debugflags, + 'debugimap!' => \$mysync->{ debugimap }, + 'debugimap1!' => \$mysync->{ acc1 }->{ debugimap }, + 'debugimap2!' => \$mysync->{ acc2 }->{ debugimap }, + 'debugdev!' => \$debugdev, + 'debugmemory!' => \$mysync->{debugmemory}, + 'debugfolders!' => \$mysync->{debugfolders}, + 'debugssl=i' => \$mysync->{debugssl}, + 'debugcgi!' => \$debugcgi, + 'debugenv!' => \$mysync->{debugenv}, + 'debugsig!' => \$mysync->{debugsig}, + 'debuglabels!' => \$mysync->{debuglabels}, + + 'simulong=f' => \$mysync->{simulong}, + 'abort' => \$mysync->{abort}, + 'abortbyfile' => \$mysync->{abortbyfile}, + + 'host1=s' => \$mysync->{ host1 }, + 'host2=s' => \$mysync->{ host2 }, + 'port1=i' => \$mysync->{ port1 }, + 'port2=i' => \$mysync->{ port2 }, + 'inet4|ipv4' => \$mysync->{ inet4 }, + 'inet6|ipv6' => \$mysync->{ inet6 }, + 'user1=s' => \$mysync->{ user1 }, + 'user2=s' => \$mysync->{ user2 }, + 'gmail1' => \$mysync->{gmail1}, + 'gmail2' => \$mysync->{gmail2}, + 'office1' => \$mysync->{office1}, + 'office2' => \$mysync->{office2}, + 'exchange1' => \$mysync->{exchange1}, + 'exchange2' => \$mysync->{exchange2}, + 'domino1' => \$mysync->{domino1}, + 'domino2' => \$mysync->{domino2}, + 'domain1=s' => \$mysync->{ acc1 }->{ domain }, + 'domain2=s' => \$mysync->{ acc2 }->{ domain }, + 'password1=s' => \$mysync->{password1}, + 'password2=s' => \$mysync->{password2}, + 'passfile1=s' => \$mysync->{ passfile1 }, + 'passfile2=s' => \$mysync->{ passfile2 }, + 'authmd5!' => \$authmd5, + 'authmd51!' => \$authmd51, + 'authmd52!' => \$authmd52, + + 'trylogin!' => \$mysync->{ trylogin }, + + 'oauthdirect1=s' => \$mysync->{ acc1 }->{ oauthdirect }, + 'oauthdirect2=s' => \$mysync->{ acc2 }->{ oauthdirect }, + + 'oauthaccesstoken1=s' => \$mysync->{ acc1 }->{ oauthaccesstoken }, + 'oauthaccesstoken2=s' => \$mysync->{ acc2 }->{ oauthaccesstoken }, + + 'sep1=s' => \$mysync->{ sep1 }, + 'sep2=s' => \$mysync->{ sep2 }, + 'sanitize!' => \$mysync->{ sanitize }, + 'folder=s@' => \$mysync->{ folder }, + 'folderrec=s' => \@folderrec, + 'include=s' => \@include, + 'exclude=s' => \@exclude, + 'noexclude' => \$mysync->{noexclude}, + 'folderfirst=s' => \@folderfirst, + 'folderlast=s' => \@folderlast, + 'prefix1=s' => \$prefix1, + 'prefix2=s' => \$prefix2, + 'subfolder1=s' => \$mysync->{ subfolder1 }, + 'subfolder2=s' => \$mysync->{ subfolder2 }, + 'fixslash2!' => \$mysync->{ fixslash2 }, + 'fixInboxINBOX!' => \$fixInboxINBOX, + 'regextrans2=s@' => \$mysync->{ regextrans2 }, + 'mixfolders!' => \$mixfolders, + 'skipemptyfolders!' => \$mysync->{ skipemptyfolders }, + 'regexmess=s' => \@regexmess, + 'noregexmess' => \$mysync->{noregexmess}, + 'skipmess=s' => \@skipmess, + 'pipemess=s' => \@pipemess, + 'pipemesscheck!' => \$pipemesscheck, + 'disarmreadreceipts!' => \$disarmreadreceipts, + 'regexflag=s@' => \$mysync->{ regexflag }, + 'noregexflag' => \$mysync->{ noregexflag }, + 'filterflags!' => \$mysync->{ filterflags }, + 'filterbuggyflags!' => \$mysync->{ filterbuggyflags }, + 'flagscase!' => \$mysync->{ flagscase }, + 'syncflagsaftercopy!' => \$syncflagsaftercopy, + 'resyncflags!' => \$mysync->{ resyncflags }, + 'synclabels!' => \$mysync->{ synclabels }, + 'resynclabels!' => \$mysync->{ resynclabels }, + 'delete|delete1!' => \$mysync->{ delete1 }, + 'delete2!' => \$mysync->{ delete2 }, + 'delete2duplicates!' => \$mysync->{ delete2duplicates }, + 'delete2folders!' => \$delete2folders, + 'delete2foldersonly=s' => \$delete2foldersonly, + 'delete2foldersbutnot=s' => \$delete2foldersbutnot, + 'syncinternaldates!' => \$syncinternaldates, + 'idatefromheader!' => \$idatefromheader, + 'syncacls!' => \$mysync->{ syncacls }, + 'maxsize=i' => \$mysync->{ maxsize }, + 'appendlimit=i' => \$mysync->{ appendlimit }, + 'truncmess=i' => \$mysync->{ truncmess }, + 'minsize=i' => \$minsize, + 'maxage=f' => \$maxage, + 'minage=f' => \$minage, + 'search=s' => \$search, + 'search1=s' => \$mysync->{ search1 }, + 'search2=s' => \$mysync->{ search2 }, + 'foldersizes!' => \$mysync->{ foldersizes }, + 'foldersizesatend!' => \$mysync->{ foldersizesatend }, + 'dry!' => \$mysync->{dry}, + 'dry1!' => \$mysync->{dry1}, + 'expunge1|expunge!' => \$mysync->{ expunge1 }, + 'expunge2!' => \$mysync->{ expunge2 }, + 'uidexpunge2!' => \$mysync->{ uidexpunge2 }, + 'subscribed' => \$subscribed, + 'subscribe!' => \$subscribe, + 'subscribeall|subscribe_all!' => \$subscribeall, + 'justbanner!' => \$mysync->{ justbanner }, + 'justfolders!'=> \$mysync->{ justfolders }, + 'justfoldersizes!' => \$mysync->{ justfoldersizes }, + 'version' => \$mysync->{version}, + 'help' => \$help, + 'timeout=f' => \$mysync->{timeout}, + 'timeout1=f' => \$mysync->{ acc1 }->{timeout}, + 'timeout2=f' => \$mysync->{ acc2 }->{timeout}, + 'skipheader=s' => \$mysync->{ skipheader }, + 'useheader=s' => \@useheader, + 'wholeheaderifneeded!' => \$wholeheaderifneeded, + 'messageidnodomain!' => \$messageidnodomain, + 'skipsize!' => \$skipsize, + 'allowsizemismatch!' => \$allowsizemismatch, + 'fastio1!' => \$mysync->{ acc1 }->{ fastio }, + 'fastio2!' => \$mysync->{ acc2 }->{ fastio }, + 'sslcheck!' => \$mysync->{sslcheck}, + 'ssl1!' => \$mysync->{ssl1}, + 'ssl2!' => \$mysync->{ssl2}, + 'ssl1_ssl_version=s' => \$mysync->{ acc1 }->{sslargs}->{SSL_version}, + 'ssl2_ssl_version=s' => \$mysync->{ acc2 }->{sslargs}->{SSL_version}, + 'sslargs1=s%' => \$mysync->{ acc1 }->{sslargs}, + 'sslargs2=s%' => \$mysync->{ acc2 }->{sslargs}, + 'tls1!' => \$mysync->{tls1}, + 'tls2!' => \$mysync->{tls2}, + 'uid1!' => \$uid1, + 'uid2!' => \$uid2, + 'authmech1=s' => \$mysync->{ acc1 }->{ authmech }, + 'authmech2=s' => \$mysync->{ acc2 }->{ authmech }, + 'authuser1=s' => \$mysync->{ acc1 }->{ authuser }, + 'authuser2=s' => \$mysync->{ acc2 }->{ authuser }, + 'proxyauth1' => \$mysync->{ acc1 }->{ proxyauth }, + 'proxyauth2' => \$mysync->{ acc2 }->{ proxyauth }, + 'compress1!' => \$mysync->{ acc1 }->{ compress }, + 'compress2!' => \$mysync->{ acc2 }->{ compress }, + 'keepalive1!' => \$mysync->{ acc1 }->{ keepalive }, + 'keepalive2!' => \$mysync->{ acc2 }->{ keepalive }, + 'split1=i' => \$split1, + 'split2=i' => \$split2, + 'buffersize=i' => \$buffersize, + 'reconnectretry1=i' => \$mysync->{ acc1 }->{ reconnectretry }, + 'reconnectretry2=i' => \$mysync->{ acc2 }->{ reconnectretry }, + 'tests!' => \$mysync->{ tests }, + 'testsdebug|tests_debug!' => \$mysync->{ testsdebug }, + 'testsunit=s@' => \$mysync->{testsunit}, + 'testslive!' => \$mysync->{testslive}, + 'testslive6!' => \$mysync->{testslive6}, + 'justlogin!' => \$mysync->{justlogin}, + 'justconnect!' => \$mysync->{justconnect}, + 'tmpdir=s' => \$mysync->{ tmpdir }, + 'pidfile=s' => \$mysync->{pidfile}, + 'pidfilelocking!' => \$mysync->{pidfilelocking}, + 'sigexit=s@' => \$mysync->{ sigexit }, + 'sigreconnect=s@' => \$mysync->{ sigreconnect }, + 'sigignore=s@' => \$mysync->{ sigignore }, + 'releasecheck!' => \$mysync->{releasecheck}, + 'modulesversion|modules_version!' => \$modulesversion, + 'usecache!' => \$usecache, + 'cacheaftercopy!' => \$cacheaftercopy, + 'debugcache!' => \$debugcache, + 'useuid!' => \$useuid, + 'addheader!' => \$mysync->{addheader}, + 'exitwhenover=i' => \$mysync->{ exitwhenover }, + 'checkselectable!' => \$mysync->{ checkselectable }, + 'checkfoldersexist!' => \$mysync->{ checkfoldersexist }, + 'checkmessageexists!' => \$checkmessageexists, + 'expungeaftereach!' => \$mysync->{ expungeaftereach }, + 'abletosearch!' => \$mysync->{abletosearch}, + 'abletosearch1!' => \$mysync->{abletosearch1}, + 'abletosearch2!' => \$mysync->{abletosearch2}, + 'showpasswords!' => \$mysync->{showpasswords}, + 'maxlinelength=i' => \$maxlinelength, + 'maxlinelengthcmd=s' => \$maxlinelengthcmd, + 'minmaxlinelength=i' => \$minmaxlinelength, + 'debugmaxlinelength!' => \$debugmaxlinelength, + 'fixcolonbug!' => \$fixcolonbug, + 'create_folder_old!' => \$create_folder_old, + 'maxmessagespersecond=f' => \$mysync->{maxmessagespersecond}, + 'maxbytespersecond=i' => \$mysync->{maxbytespersecond}, + 'maxbytesafter=i' => \$mysync->{maxbytesafter}, + 'maxsleep=f' => \$mysync->{maxsleep}, + 'skipcrossduplicates!' => \$skipcrossduplicates, + 'debugcrossduplicates!' => \$debugcrossduplicates, + 'log!' => \$mysync->{log}, + 'tail!' => \$mysync->{tail}, + 'logfile=s' => \$mysync->{logfile}, + 'logdir=s' => \$mysync->{logdir}, + 'errorsmax=i' => \$mysync->{errorsmax}, + 'errorsdump!' => \$mysync->{ errorsdump }, + 'fetch_hash_set=s' => \$fetch_hash_set, + 'automap!' => \$mysync->{automap}, + 'justautomap!' => \$mysync->{justautomap}, + 'id!' => \$mysync->{id}, + 'f1f2=s@' => \$mysync->{f1f2}, + 'nof1f2' => \$mysync->{nof1f2}, + 'f1f2h=s%' => \$mysync->{f1f2h}, + 'justfolderlists!' => \$mysync->{justfolderlists}, + 'delete1emptyfolders' => \$mysync->{delete1emptyfolders}, + 'checknoabletosearch!' => \$mysync->{checknoabletosearch}, + 'syncduplicates!' => \$mysync->{ syncduplicates }, + 'dockercontext!' => \$mysync->{ dockercontext }, + + + ) ; + #myprint( Data::Dumper->Dump( [ $mysync ] ) ) ; + $mysync->{ debug } and output( $mysync, "get options: [$opt_ret][$numopt]\n" ) ; + my $numopt_after = scalar @arguments ; + #myprint( "get options: [$opt_ret][$numopt][$numopt_after]\n" ) ; + + # The $arguments[0] test is just because parallel adds "" when it is + # used with {=7=} in sync_parallel_unix.sh + if ( $numopt_after and $arguments[0] ) { + myprint( + "Found ", scalar( @arguments ), " extra arguments : [@arguments]\n", + "It usually means a quoting issue in the command line ", + "or some misspelling options.\n", + ) ; + return ; + } + if ( ! $opt_ret ) { + return ; + } + return $numopt ; +} + + + +sub tests_get_options +{ + note( 'Entering tests_get_options()' ) ; + + # CAVEAT: still setting global variables, be careful + # with tests, the context increases! $debug stays on for example. + # API: + # * input arguments: two ways, command line or CGI + # * the program arguments + # * QUERY_STRING env variable + # * return + # * undef if bad things happened like + # * options not known + # * --delete 2 input + # * number of arguments or QUERY_STRING length + my $mysync = { } ; + is( undef, get_options( $mysync, qw( --noexist ) ), 'get_options: --noexist => undef' ) ; + is( undef, $mysync->{ noexist }, 'get_options: --noexist => undef' ) ; + $mysync = { } ; + is( undef, get_options( $mysync, qw( --lalala --noexist --version ) ), 'get_options: --lalala --noexist --version => undef' ) ; + is( 1, $mysync->{ version }, 'get_options: --version => 1' ) ; + is( undef, $mysync->{ noexist }, 'get_options: --noexist => undef' ) ; + $mysync = { } ; + is( 1, get_options( $mysync, qw( --delete2 ) ), 'get_options: --delete2 => 1' ) ; + is( 1, $mysync->{ delete2 }, 'get_options: --delete2 => var delete2 = 1' ) ; + $mysync = { } ; + is( undef, get_options( $mysync, qw( --delete 2 ) ), 'get_options: --delete 2 => var undef' ) ; + is( undef, $mysync->{ delete1 }, 'get_options: --delete 2 => var still undef ; good!' ) ; + $mysync = { } ; + is( undef, get_options( $mysync, "--delete 2" ), 'get_options: --delete 2 => undef' ) ; + + is( 1, get_options( $mysync, "--version" ), 'get_options: --version => 1' ) ; + is( 1, get_options( $mysync, "--help" ), 'get_options: --help => 1' ) ; + + is( undef, get_options( $mysync, qw( --noexist --version ) ), 'get_options: --debug --noexist --version => undef' ) ; + is( 1, get_options( $mysync, qw( --version ) ), 'get_options: --version => 1' ) ; + is( undef, get_options( $mysync, qw( extra ) ), 'get_options: extra => undef' ) ; + is( undef, get_options( $mysync, qw( extra1 --version extra2 ) ), 'get_options: extra1 --version extra2 => undef' ) ; + + $mysync = { } ; + is( 2, get_options( $mysync, qw( --host1 HOST_01) ), 'get_options: --host1 HOST_01 => 1' ) ; + is( 'HOST_01', $mysync->{ host1 }, 'get_options: --host1 HOST_01 => HOST_01' ) ; + #myprint( Data::Dumper->Dump( [ $mysync ] ) ) ; + + note( 'Leaving tests_get_options()' ) ; + return ; +} + + + +sub get_options +{ + my $mysync = shift @ARG ; + my @arguments = @ARG ; + #myprint( "1 mysync: ", Data::Dumper->Dump( [ $mysync ] ) ) ; + my $ret ; + if ( under_cgi_context( ) ) { + # CGI context + $ret = get_options_cgi( $mysync, @arguments ) ; + }else{ + # Command line context ; + $ret = get_options_cmd( $mysync, @arguments ) ; + } ; + #myprint( "2 mysync: ", Data::Dumper->Dump( [ $mysync ] ) ) ; + + foreach my $key ( sort keys %{ $mysync } ) { + if ( ! defined $mysync->{$key} ) { + delete $mysync->{$key} ; + next ; + } + if ( 'ARRAY' eq ref( $mysync->{$key} ) + and 0 == scalar( @{ $mysync->{$key} } ) ) { + delete $mysync->{$key} ; + } + } + return $ret ; +} + + +sub tests_infos +{ + note( 'Entering tests_infos()' ) ; + note( "OSNAME=$OSNAME" ) ; + note( "hostname=". hostname() ) ; + note( "cwd=" . getcwd( ) ) ; + note( "PROGRAM_NAME=$PROGRAM_NAME" ) ; + my $stat = stat("$PROGRAM_NAME") ; + my $perms = sprintf( "%04o\n", $stat->mode & oct($PERMISSION_FILTER) ) ; + note( "permissions=$perms" ) ; + note( "PROCESS_ID=$PROCESS_ID" ) ; + note( "REAL_USER_ID=$REAL_USER_ID" ) ; + note( "EFFECTIVE_USER_ID=$EFFECTIVE_USER_ID" ) ; + note( "context: " . imapsync_context( $sync ) ) ; + note( "memory_consumption: " . memory_consumption() . " bytes aka " . bytes_display_string_dec( memory_consumption() ) ) ; + cpu_number + note( "cpu_number: " . cpu_number() ) ; + note( $sync->{rcs} ) ; + + note( 'Leaving tests_infos()' ) ; + return ; +} + + + +sub condition_to_leave_after_tests +{ + my $mysync = shift ; + if ( $mysync->{ testslive } or $mysync->{ testslive6 } ) + { + return 0 ; + } + + if ( $mysync->{ tests } + or $mysync->{ testsdebug } + or $mysync->{ testsunit } + ) + { + return 1 ; + } +} + + +sub testunitsession +{ + my $mysync = shift ; + + if ( ! $mysync ) { return ; } + if ( ! $mysync->{ testsunit } ) { return ; } + + my @functions = @{ $mysync->{ testsunit } } ; + + if ( ! @functions ) { return ; } + + SKIP: { + if ( ! @functions ) { skip 'No test in normal run' ; } + testsunit( @functions ) ; + done_testing( ) ; + } + return ; +} + +sub tests_count_0s +{ + note( 'Entering tests_count_zeros()' ) ; + is( 0, count_0s( ), 'count_0s: no parameters => 0' ) ; + is( 1, count_0s( 0 ), 'count_0s: 0 => 1' ) ; + is( 0, count_0s( 1 ), 'count_0s: 1 => 0' ) ; + is( 1, count_0s( 1, 0, 1 ), 'count_0s: 1, 0, 1 => 1' ) ; + is( 2, count_0s( 1, 0, 1, 0 ), 'count_0s: 1, 0, 1, 0 => 2' ) ; + note( 'Leaving tests_count_zeros()' ) ; + return ; +} +sub count_0s +{ + my @array = @ARG ; + + if ( ! @array ) { return 0 ; } + my $nb_zeros = 0 ; + map { $_ == 0 and $nb_zeros += 1 } @array ; + return $nb_zeros ; +} + +sub tests_report_failures +{ + note( 'Entering tests_report_failures()' ) ; + + is( undef, report_failures( ), 'report_failures: no parameters => undef' ) ; + is( "nb 1 - first\n", report_failures( ({'ok' => 0, name => 'first'}) ), 'report_failures: "first" failed => nb 1 - first' ) ; + is( q{}, report_failures( ( {'ok' => 1, name => 'first'} ) ), 'report_failures: "first" success =>' ) ; + is( "nb 2 - second\n", report_failures( ( {'ok' => 1, name => 'second'}, {'ok' => 0, name => 'second'} ) ), 'report_failures: "second" failed => nb 2 - second' ) ; + is( "nb 1 - first\nnb 2 - second\n", report_failures( ( {'ok' => 0, name => 'first'}, {'ok' => 0, name => 'second'} ) ), 'report_failures: both failed => nb 1 - first nb 2 - second' ) ; + note( 'Leaving tests_report_failures()' ) ; + return ; +} + +sub report_failures +{ + my @details = @ARG ; + + if ( ! @details ) { return ; } + + my $counter = 1 ; + my $report = q{} ; + foreach my $details ( @details ) { + if ( ! $details->{ 'ok' } ) { + my $name = $details->{ 'name' } || 'NONAME' ; + $report .= "nb $counter - $name\n" ; + } + $counter += 1 ; + } + return $report ; + +} + +sub tests_true +{ + note( 'Entering tests_true()' ) ; + + is( 1, 1, 'true: 1 is 1' ) ; + note( 'Leaving tests_true()' ) ; + return ; +} + +sub tests_testsunit +{ + note( 'Entering tests_testunit()' ) ; + + is( undef, testsunit( ), 'testsunit: no parameters => undef' ) ; + is( undef, testsunit( undef ), 'testsunit: an undef parameter => undef' ) ; + is( undef, testsunit( q{} ), 'testsunit: an empty parameter => undef' ) ; + is( undef, testsunit( 'idonotexist' ), 'testsunit: a do not exist function as parameter => undef' ) ; + is( undef, testsunit( 'tests_true' ), 'testsunit: tests_true => undef' ) ; + note( 'Leaving tests_testunit()' ) ; + return ; +} + +sub testsunit +{ + my @functions = @ARG ; + + if ( ! @functions ) { # + myprint( "testsunit warning: no argument given\n" ) ; + return ; + } + + foreach my $function ( @functions ) { + if ( ! $function ) { + myprint( "testsunit warning: argument is empty\n" ) ; + next ; + } + if ( ! exists &$function ) { + myprint( "testsunit warning: function $function does not exist\n" ) ; + next ; + } + if ( ! defined &$function ) { + myprint( "testsunit warning: function $function is not defined\n" ) ; + next ; + } + my $function_ref = \&{ $function } ; + &$function_ref() ; + } + return ; +} + +sub testsdebug +{ + # Now a little obsolete since there is + # imapsync ... --testsunit "anyfunction" + my $mysync = shift ; + if ( ! $mysync->{ testsdebug } ) { return ; } + SKIP: { + if ( ! $mysync->{ testsdebug } ) { + skip 'No test in normal run' ; + } + + note( 'Entering testsdebug()' ) ; + #ok( ( ( not -d 'W/tmp/tests' ) or rmtree( 'W/tmp/tests/' ) ), 'testsdebug: rmtree W/tmp/tests' ) ; + #tests_check_binary_embed_all_dyn_libs( ) ; + #tests_killpid_by_parent( ) ; + #tests_killpid_by_brother( ) ; + #tests_kill_zero( ) ; + #tests_connect_socket( ) ; + #tests_probe_imapssl( ) ; + #tests_cpu_number( ) ; + #tests_mailimapclient_connect( ) ; + tests_loadavg( ) ; + #tests_always_fail( ) ; + + note( 'Leaving testsdebug()' ) ; + done_testing( ) ; + } + return ; +} + + +sub tests +{ + my $mysync = shift ; + if ( ! $mysync->{ tests } ) { return ; } + + SKIP: { + skip 'No test in normal run' if ( ! $mysync->{ tests } ) ; + note( 'Entering tests()' ) ; + tests_folder_routines( ) ; + tests_compare_lists( ) ; + tests_regexmess( ) ; + tests_skipmess( ) ; + tests_regexflags( ); + tests_ucsecond( ) ; + tests_permanentflags(); + tests_flags_filter( ) ; + tests_separator_invert( ) ; + tests_imap2_folder_name( ) ; + tests_command_line_nopassword( ) ; + tests_good_date( ) ; + tests_max( ) ; + tests_remove_not_num(); + tests_memory_consumption( ) ; + tests_is_a_release_number(); + tests_imapsync_basename(); + tests_list_keys_in_2_not_in_1(); + tests_convert_sep_to_slash( ) ; + tests_match_a_cache_file( ) ; + tests_cache_map( ) ; + tests_get_cache( ) ; + tests_clean_cache( ) ; + tests_clean_cache_2( ) ; + tests_touch( ) ; + tests_flagscase( ) ; + tests_mkpath( ) ; + tests_extract_header( ) ; + tests_decompose_header( ) ; + tests_epoch( ) ; + tests_add_header( ) ; + tests_cache_dir_fix( ) ; + tests_cache_dir_fix_win( ) ; + tests_filter_forbidden_characters( ) ; + tests_cache_folder( ) ; + tests_time_remaining( ) ; + tests_decompose_regex( ) ; + tests_backtick( ) ; + tests_bytes_display_string_bin( ) ; + tests_bytes_display_string_dec( ) ; + tests_header_line_normalize( ) ; + tests_fix_Inbox_INBOX_mapping( ) ; + tests_max_line_length( ) ; + tests_subject( ) ; + tests_msgs_from_maxmin( ) ; + tests_tmpdir_has_colon_bug( ) ; + tests_sleep_max_messages( ) ; + tests_sleep_max_bytes( ) ; + tests_logfile( ) ; + tests_setlogfile( ) ; + tests_jux_utf8_old( ) ; + tests_jux_utf8( ) ; + tests_pipemess( ) ; + tests_jux_utf8_list( ) ; + tests_guess_prefix( ) ; + tests_guess_separator( ) ; + tests_format_for_imap_arg( ) ; + tests_imapsync_id( ) ; + tests_date_from_rcs( ) ; + tests_quota_extract_storage_limit_in_bytes( ) ; + tests_quota_extract_storage_current_in_bytes( ) ; + tests_guess_special( ) ; + tests_do_valid_directory( ) ; + tests_delete1emptyfolders( ) ; + tests_message_for_host2( ) ; + tests_length_ref( ) ; + tests_firstline( ) ; + tests_diff_or_NA( ) ; + tests_match_number( ) ; + tests_all_defined( ) ; + tests_special_from_folders_hash( ) ; + tests_notmatch( ) ; + tests_match( ) ; + tests_get_options( ) ; + tests_get_options_cgi_context( ) ; + tests_rand32( ) ; + tests_hashsynclocal( ) ; + tests_hashsync( ) ; + tests_output( ) ; + tests_output_reset_with( ) ; + tests_output_start( ) ; + tests_check_last_release( ) ; + tests_loadavg( ) ; + tests_cpu_number( ) ; + tests_load_and_delay( ) ; + #tests_imapsping( ) ; + #tests_tcpping( ) ; + tests_sslcheck( ) ; + tests_not_long_imapsync_version_public( ) ; + tests_reconnect_if_needed( ) ; + tests_reconnect_12_if_needed( ) ; + tests_sleep_if_needed( ) ; + tests_string_to_file( ) ; + tests_file_to_string( ) ; + tests_under_cgi_context( ) ; + tests_umask( ) ; + tests_umask_str( ) ; + tests_set_umask( ) ; + tests_createhashfileifneeded( ) ; + tests_slash_to_underscore( ) ; + tests_testsunit( ) ; + tests_count_0s( ) ; + tests_report_failures( ) ; + tests_min( ) ; + #tests_connect_socket( ) ; + #tests_resolvrev( ) ; + tests_usage( ) ; + tests_version_from_rcs( ) ; + tests_backslash_caret( ) ; + #tests_mailimapclient_connect_bug( ) ; # it fails with Mail-IMAPClient <= 3.39 + tests_write_pidfile( ) ; + tests_remove_pidfile_not_running( ) ; + tests_match_a_pid_number( ) ; + tests_prefix_seperator_invertion( ) ; + tests_is_integer( ) ; + tests_integer_or_1( ) ; + tests_is_number( ) ; + tests_sig_install( ) ; + tests_template( ) ; + tests_split_around_equal( ) ; + tests_toggle_sleep( ) ; + tests_labels( ) ; + tests_synclabels( ) ; + tests_uidexpunge_or_expunge( ) ; + tests_appendlimit_from_capability( ) ; + tests_maxsize_setting( ) ; + tests_mock_capability( ) ; + tests_appendlimit( ) ; + tests_capability_of( ) ; + tests_search_in_array( ) ; + tests_operators_and_exclam_precedence( ) ; + tests_teelaunch( ) ; + tests_logfileprepa( ) ; + tests_useheader_suggestion( ) ; + tests_nb_messages_in_2_not_in_1( ) ; + tests_labels_add_subfolder2( ) ; + tests_labels_remove_subfolder1( ) ; + tests_resynclabels( ) ; + tests_labels_remove_special( ) ; + tests_uniq( ) ; + tests_remove_from_requested_folders( ) ; + tests_errors_log( ) ; + tests_add_subfolder1_to_folderrec( ) ; + tests_sanitize_subfolder( ) ; + tests_remove_edging_blanks( ) ; + tests_sanitize( ) ; + tests_remove_last_char_if_is( ) ; + tests_check_binary_embed_all_dyn_libs( ) ; + tests_nthline( ) ; + tests_secondline( ) ; + tests_tail( ) ; + tests_truncmess( ) ; + tests_eta( ) ; + tests_timesince( ) ; + tests_timenext( ) ; + tests_foldersize( ) ; + tests_imapsync_context( ) ; + tests_abort( ) ; + tests_probe_imapssl( ) ; + tests_mailimapclient_connect( ) ; + tests_checknoabletosearch( ) ; + tests_errorsdump( ) ; + tests_errorsanalyse( ) ; + tests_most_common_error( ) ; + tests_errorclassify( ) ; + tests_error_type( ) ; + tests_sanitize_host( ) ; + tests_hmac_sha1_hex( ) ; + tests_total_bytes_max_reached( ) ; + tests_header_construct( ) ; + tests_remove_doublequotes_if_any( ) ; + tests_login_imap( ) ; + tests_login_imap_oauth( ) ; + tests_skipmess_neg( ) ; + tests_localtimez( ) ; + tests_file_to_array( ) ; + tests_cpu_time( ) ; + tests_cpu_percent( ) ; + tests_cpu_percent_global( ) ; + tests_flags_for_host2( ) ; + tests_under_docker_context( ) ; + tests_exit_value( ) ; + tests_comment_of_error_type( ) ; + tests_debugcontent( ) ; + tests_compress_ssl( ) ; + tests_compress( ) ; + tests_get_options_extra( ) ; + tests_get_options_from_string( ) ; + tests_infos( ) ; + #tests_resolv( ) ; + + # Those three are for later use, when webserver will be inside imapsync + # or will be deleted them if I abandon the project. + #tests_killpid_by_parent( ) ; + #tests_killpid_by_brother( ) ; + #tests_kill_zero( ) ; + + #tests_always_fail( ) ; + done_testing( 1860 ) ; + note( 'Leaving tests()' ) ; + } + return ; +} + +sub tests_template +{ + note( 'Entering tests_template()' ) ; + + is( undef, template( ), 'template: no args => undef' ) ; + my $mysync = { } ; + is( undef, template( $mysync ), 'template: undef => undef' ) ; + is_deeply( {}, {}, 'template: a hash is a hash' ) ; + is_deeply( [], [], 'template: an array is an array' ) ; + + note( 'Leaving tests_template()' ) ; + return ; +} + +sub template +{ + my $mysync = shift @ARG ; + + return ; +} diff --git a/mailcow/data/Dockerfiles/dovecot/imapsync_runner.pl b/mailcow/data/Dockerfiles/dovecot/imapsync_runner.pl new file mode 100644 index 0000000..9eaf5f4 --- /dev/null +++ b/mailcow/data/Dockerfiles/dovecot/imapsync_runner.pl @@ -0,0 +1,196 @@ +#!/usr/bin/perl + +use DBI; +use LockFile::Simple qw(lock trylock unlock); +use Proc::ProcessTable; +use Data::Dumper qw(Dumper); +use IPC::Run 'run'; +use File::Temp; +use Try::Tiny; +use sigtrap 'handler' => \&sig_handler, qw(INT TERM KILL QUIT); + +sub trim { my $s = shift; $s =~ s/^\s+|\s+$//g; return $s }; +my $t = Proc::ProcessTable->new; +my $imapsync_running = grep { $_->{cmndline} =~ /imapsync\s/i } @{$t->table}; +if ($imapsync_running ge 1) +{ + print "imapsync is active, exiting..."; + exit; +} + +sub qqw($) { + my @params = (); + my @values = split(/(?=--)/, $_[0]); + foreach my $val (@values) { + my @tmpparam = split(/ /, $val, 2); + foreach my $tmpval (@tmpparam) { + if ($tmpval ne '') { + push @params, $tmpval; + } + } + } + foreach my $val (@params) { + $val=trim($val); + } + return @params; +} + +$run_dir="/tmp"; +$dsn = 'DBI:mysql:database=' . $ENV{'DBNAME'} . ';mysql_socket=/var/run/mysqld/mysqld.sock'; +$lock_file = $run_dir . "/imapsync_busy"; +$lockmgr = LockFile::Simple->make(-autoclean => 1, -max => 1); +$lockmgr->lock($lock_file) || die "can't lock ${lock_file}"; +$dbh = DBI->connect($dsn, $ENV{'DBUSER'}, $ENV{'DBPASS'}, { + mysql_auto_reconnect => 1, + mysql_enable_utf8mb4 => 1 +}); +$dbh->do("UPDATE imapsync SET is_running = 0"); + +sub sig_handler { + # Send die to force exception in "run" + die "sig_handler received signal, preparing to exit...\n"; +}; + +open my $file, '<', "/etc/sogo/sieve.creds"; +my $creds = <$file>; +close $file; +my ($master_user, $master_pass) = split /:/, $creds; +my $sth = $dbh->prepare("SELECT id, + user1, + user2, + host1, + authmech1, + password1, + exclude, + port1, + enc1, + delete2duplicates, + maxage, + subfolder2, + delete1, + delete2, + automap, + skipcrossduplicates, + maxbytespersecond, + custom_params, + subscribeall, + timeout1, + timeout2, + dry + FROM imapsync + WHERE active = 1 + AND is_running = 0 + AND ( + UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_run) > mins_interval * 60 + OR + last_run IS NULL) + ORDER BY last_run"); + +$sth->execute(); +my $row; + +while ($row = $sth->fetchrow_arrayref()) { + + $id = @$row[0]; + $user1 = @$row[1]; + $user2 = @$row[2]; + $host1 = @$row[3]; + $authmech1 = @$row[4]; + $password1 = @$row[5]; + $exclude = @$row[6]; + $port1 = @$row[7]; + $enc1 = @$row[8]; + $delete2duplicates = @$row[9]; + $maxage = @$row[10]; + $subfolder2 = @$row[11]; + $delete1 = @$row[12]; + $delete2 = @$row[13]; + $automap = @$row[14]; + $skipcrossduplicates = @$row[15]; + $maxbytespersecond = @$row[16]; + $custom_params = @$row[17]; + $subscribeall = @$row[18]; + $timeout1 = @$row[19]; + $timeout2 = @$row[20]; + $dry = @$row[21]; + + if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; } + + my $template = $run_dir . '/imapsync.XXXXXXX'; + my $passfile1 = File::Temp->new(TEMPLATE => $template); + my $passfile2 = File::Temp->new(TEMPLATE => $template); + + binmode( $passfile1, ":utf8" ); + + print $passfile1 "$password1\n"; + print $passfile2 trim($master_pass) . "\n"; + + my @custom_params_a = qqw($custom_params); + my $custom_params_ref = \@custom_params_a; + + my $generated_cmds = [ "/usr/local/bin/imapsync", + "--tmpdir", "/tmp", + "--nofoldersizes", + "--addheader", + ($timeout1 gt "0" ? () : ('--timeout1', $timeout1)), + ($timeout2 gt "0" ? () : ('--timeout2', $timeout2)), + ($exclude eq "" ? () : ("--exclude", $exclude)), + ($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)), + ($maxage eq "0" ? () : ('--maxage', $maxage)), + ($maxbytespersecond eq "0" ? () : ('--maxbytespersecond', $maxbytespersecond)), + ($delete2duplicates ne "1" ? () : ('--delete2duplicates')), + ($subscribeall ne "1" ? () : ('--subscribeall')), + ($delete1 ne "1" ? () : ('--delete')), + ($delete2 ne "1" ? () : ('--delete2')), + ($automap ne "1" ? () : ('--automap')), + ($skipcrossduplicates ne "1" ? () : ('--skipcrossduplicates')), + (!defined($enc1) ? () : ($enc1)), + "--host1", $host1, + "--user1", $user1, + "--passfile1", $passfile1->filename, + "--port1", $port1, + "--host2", "localhost", + "--user2", $user2 . '*' . trim($master_user), + "--passfile2", $passfile2->filename, + ($dry eq "1" ? ('--dry') : ()), + '--no-modulesversion', + '--noreleasecheck']; + + try { + $is_running = $dbh->prepare("UPDATE imapsync SET is_running = 1, success = NULL, exit_status = NULL WHERE id = ?"); + $is_running->bind_param( 1, ${id} ); + $is_running->execute(); + + run [@$generated_cmds, @$custom_params_ref], '&>', \my $stdout; + + # check exit code and status + ($exit_code, $exit_status) = ($stdout =~ m/Exiting\swith\sreturn\svalue\s(\d+)\s\(([^:)]+)/); + + $success = 0; + if (defined $exit_code && $exit_code == 0) { + $success = 1; + } + + $update = $dbh->prepare("UPDATE imapsync SET returned_text = ?, success = ?, exit_status = ? WHERE id = ?"); + $update->bind_param( 1, ${stdout} ); + $update->bind_param( 2, ${success} ); + $update->bind_param( 3, ${exit_status} ); + $update->bind_param( 4, ${id} ); + $update->execute(); + } catch { + $update = $dbh->prepare("UPDATE imapsync SET returned_text = 'Could not start or finish imapsync', success = 0 WHERE id = ?"); + $update->bind_param( 1, ${id} ); + $update->execute(); + } finally { + $update = $dbh->prepare("UPDATE imapsync SET last_run = NOW(), is_running = 0 WHERE id = ?"); + $update->bind_param( 1, ${id} ); + $update->execute(); + }; + + +} + +$sth->finish(); +$dbh->disconnect(); + +$lockmgr->unlock($lock_file); diff --git a/mailcow/data/Dockerfiles/dovecot/maildir_gc.sh b/mailcow/data/Dockerfiles/dovecot/maildir_gc.sh new file mode 100755 index 0000000..21358cc --- /dev/null +++ b/mailcow/data/Dockerfiles/dovecot/maildir_gc.sh @@ -0,0 +1,2 @@ +#!/bin/bash +[ -d /var/vmail/_garbage/ ] && /usr/bin/find /var/vmail/_garbage/ -mindepth 1 -maxdepth 1 -type d -cmin +${MAILDIR_GC_TIME} -exec rm -r {} \; diff --git a/mailcow/data/Dockerfiles/dovecot/optimize-fts.sh b/mailcow/data/Dockerfiles/dovecot/optimize-fts.sh new file mode 100644 index 0000000..c19d6e5 --- /dev/null +++ b/mailcow/data/Dockerfiles/dovecot/optimize-fts.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +if [[ "${SKIP_FTS}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + exit 0 +else + doveadm fts optimize -A +fi diff --git a/mailcow/data/Dockerfiles/dovecot/quarantine_notify.py b/mailcow/data/Dockerfiles/dovecot/quarantine_notify.py new file mode 100755 index 0000000..dfcb1f2 --- /dev/null +++ b/mailcow/data/Dockerfiles/dovecot/quarantine_notify.py @@ -0,0 +1,168 @@ +#!/usr/bin/python3 + +import smtplib +import os +import sys +import MySQLdb +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.utils import COMMASPACE, formatdate +import jinja2 +from jinja2 import Template +import json +import redis +import time +import html2text +import socket + +pid = str(os.getpid()) +pidfile = "/tmp/quarantine_notify.pid" + +if os.path.isfile(pidfile): + print("%s already exists, exiting" % (pidfile)) + sys.exit() + +pid = str(os.getpid()) +f = open(pidfile, 'w') +f.write(pid) +f.close() + +try: + + while True: + try: + r = redis.StrictRedis(host='redis', decode_responses=True, port=6379, db=0, password=os.environ['REDISPASS']) + r.ping() + except Exception as ex: + print('%s - trying again...' % (ex)) + time.sleep(3) + else: + break + + time_now = int(time.time()) + mailcow_hostname = os.environ.get('MAILCOW_HOSTNAME') + + max_score = float(r.get('Q_MAX_SCORE') or "9999.0") + if max_score == "": + max_score = 9999.0 + + def query_mysql(query, headers = True, update = False): + while True: + try: + cnx = MySQLdb.connect(user=os.environ.get('DBUSER'), password=os.environ.get('DBPASS'), database=os.environ.get('DBNAME'), charset="utf8mb4", collation="utf8mb4_general_ci") + except Exception as ex: + print('%s - trying again...' % (ex)) + time.sleep(3) + else: + break + cur = cnx.cursor() + cur.execute(query) + if not update: + result = [] + columns = tuple( [d[0] for d in cur.description] ) + for row in cur: + if headers: + result.append(dict(list(zip(columns, row)))) + else: + result.append(row) + cur.close() + cnx.close() + return result + else: + cnx.commit() + cur.close() + cnx.close() + + def notify_rcpt(rcpt, msg_count, quarantine_acl, category): + if category == "add_header": category = "add header" + meta_query = query_mysql('SELECT SHA2(CONCAT(id, qid), 256) AS qhash, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = "%s" AND score < %f AND (action = "%s" OR "all" = "%s")' % (rcpt, max_score, category, category)) + print("%s: %d of %d messages qualify for notification" % (rcpt, len(meta_query), msg_count)) + if len(meta_query) == 0: + return + msg_count = len(meta_query) + if r.get('Q_HTML'): + try: + template = Template(r.get('Q_HTML')) + except: + print("Error: Cannot parse quarantine template, falling back to default template.") + with open('/templates/quarantine.tpl') as file_: + template = Template(file_.read()) + else: + with open('/templates/quarantine.tpl') as file_: + template = Template(file_.read()) + html = template.render(meta=meta_query, username=rcpt, counter=msg_count, hostname=mailcow_hostname, quarantine_acl=quarantine_acl) + text = html2text.html2text(html) + count = 0 + while count < 15: + count += 1 + try: + server = smtplib.SMTP('postfix', 590, 'quarantine') + server.ehlo() + msg = MIMEMultipart('alternative') + msg_from = r.get('Q_SENDER') or "quarantine@localhost" + # Remove non-ascii chars from field + msg['From'] = ''.join([i if ord(i) < 128 else '' for i in msg_from]) + msg['Subject'] = r.get('Q_SUBJ') or "Spam Quarantine Notification" + msg['Date'] = formatdate(localtime = True) + text_part = MIMEText(text, 'plain', 'utf-8') + html_part = MIMEText(html, 'html', 'utf-8') + msg.attach(text_part) + msg.attach(html_part) + msg['To'] = str(rcpt) + bcc = r.get('Q_BCC') or "" + redirect = r.get('Q_REDIRECT') or "" + text = msg.as_string() + if bcc == '': + if redirect == '': + server.sendmail(msg['From'], str(rcpt), text) + else: + server.sendmail(msg['From'], str(redirect), text) + else: + if redirect == '': + server.sendmail(msg['From'], [str(rcpt)] + [str(bcc)], text) + else: + server.sendmail(msg['From'], [str(redirect)] + [str(bcc)], text) + server.quit() + for res in meta_query: + query_mysql('UPDATE quarantine SET notified = 1 WHERE id = "%d"' % (res['id']), update = True) + r.hset('Q_LAST_NOTIFIED', record['rcpt'], time_now) + break + except Exception as ex: + server.quit() + print('%s' % (ex)) + time.sleep(3) + + records = query_mysql('SELECT IFNULL(user_acl.quarantine, 0) AS quarantine_acl, count(id) AS counter, rcpt FROM quarantine LEFT OUTER JOIN user_acl ON user_acl.username = rcpt WHERE notified = 0 AND score < %f AND rcpt in (SELECT username FROM mailbox) GROUP BY rcpt' % (max_score)) + + for record in records: + attrs = '' + attrs_json = '' + time_trans = { + "hourly": 3600, + "daily": 86400, + "weekly": 604800 + } + try: + last_notification = int(r.hget('Q_LAST_NOTIFIED', record['rcpt'])) + if last_notification > time_now: + print('Last notification is > time now, assuming never') + last_notification = 0 + except Exception as ex: + print('Could not determine last notification for %s, assuming never' % (record['rcpt'])) + last_notification = 0 + attrs_json = query_mysql('SELECT attributes FROM mailbox WHERE username = "%s"' % (record['rcpt'])) + attrs = attrs_json[0]['attributes'] + if isinstance(attrs, str): + # if attr is str then just load it + attrs = json.loads(attrs) + else: + # if it's bytes then decode and load it + attrs = json.loads(attrs.decode('utf-8')) + if attrs['quarantine_notification'] not in ('hourly', 'daily', 'weekly'): + continue + if last_notification == 0 or (last_notification + time_trans[attrs['quarantine_notification']]) <= time_now: + print("Notifying %s: Considering %d new items in quarantine (policy: %s)" % (record['rcpt'], record['counter'], attrs['quarantine_notification'])) + notify_rcpt(record['rcpt'], record['counter'], record['quarantine_acl'], attrs['quarantine_category']) + +finally: + os.unlink(pidfile) diff --git a/mailcow/data/Dockerfiles/dovecot/quota_notify.py b/mailcow/data/Dockerfiles/dovecot/quota_notify.py new file mode 100755 index 0000000..598134e --- /dev/null +++ b/mailcow/data/Dockerfiles/dovecot/quota_notify.py @@ -0,0 +1,94 @@ +#!/usr/bin/python3 + +import smtplib +import os +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.utils import COMMASPACE, formatdate +import jinja2 +from jinja2 import Template +import redis +import time +import json +import sys +import html2text +from subprocess import Popen, PIPE, STDOUT + +if len(sys.argv) > 2: + percent = int(sys.argv[1]) + username = str(sys.argv[2]) +else: + print("Args missing") + sys.exit(1) + +while True: + try: + r = redis.StrictRedis(host='redis', decode_responses=True, port=6379, db=0, username='quota_notify', password='') + r.ping() + except Exception as ex: + print('%s - trying again...' % (ex)) + time.sleep(3) + else: + break + +if r.get('QW_HTML'): + try: + template = Template(r.get('QW_HTML')) + except: + print("Error: Cannot parse quarantine template, falling back to default template.") + with open('/templates/quota.tpl') as file_: + template = Template(file_.read()) +else: + with open('/templates/quota.tpl') as file_: + template = Template(file_.read()) + +html = template.render(username=username, percent=percent) +text = html2text.html2text(html) + +try: + msg = MIMEMultipart('alternative') + msg['From'] = r.get('QW_SENDER') or "quota-warning@localhost" + msg['Subject'] = r.get('QW_SUBJ') or "Quota warning" + msg['Date'] = formatdate(localtime = True) + text_part = MIMEText(text, 'plain', 'utf-8') + html_part = MIMEText(html, 'html', 'utf-8') + msg.attach(text_part) + msg.attach(html_part) + msg['To'] = username + p = Popen(['/usr/libexec/dovecot/dovecot-lda', '-d', username, '-o', '"plugin/quota=maildir:User quota:noenforcing"'], stdout=PIPE, stdin=PIPE, stderr=STDOUT) + p.communicate(input=bytes(msg.as_string(), 'utf-8')) + + domain = username.split("@")[-1] + if domain and r.hget('QW_BCC', domain): + bcc_data = json.loads(r.hget('QW_BCC', domain)) + bcc_rcpts = bcc_data['bcc_rcpts'] + if bcc_data['active'] == 1: + for rcpt in bcc_rcpts: + msg = MIMEMultipart('alternative') + msg['From'] = username + subject = r.get('QW_SUBJ') or "Quota warning" + msg['Subject'] = subject + ' (' + username + ')' + msg['Date'] = formatdate(localtime = True) + text_part = MIMEText(text, 'plain', 'utf-8') + html_part = MIMEText(html, 'html', 'utf-8') + msg.attach(text_part) + msg.attach(html_part) + msg['To'] = rcpt + server = smtplib.SMTP('postfix', 588, 'quotanotification') + server.ehlo() + server.sendmail(msg['From'], str(rcpt), msg.as_string()) + server.quit() + +except Exception as ex: + print('Failed to send quota notification: %s' % (ex)) + sys.exit(1) + +try: + sys.stdout.close() +except: + pass + +try: + sys.stderr.close() +except: + pass diff --git a/mailcow/data/Dockerfiles/dovecot/repl_health.sh b/mailcow/data/Dockerfiles/dovecot/repl_health.sh new file mode 100755 index 0000000..2d7674b --- /dev/null +++ b/mailcow/data/Dockerfiles/dovecot/repl_health.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +source /source_env.sh + +# Do not attempt to write to slave +if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then + REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning" +else + REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning" +fi + +# Is replication active? +# grep on file is less expensive than doveconf +if [ -n ${MAILCOW_REPLICA_IP} ]; then + ${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null + exit +fi + +FAILED_SYNCS=$(doveadm replicator status | grep "Waiting 'failed' requests" | grep -oE '[0-9]+') + +# Set amount of failed jobs as DOVECOT_REPL_HEALTH +# 1 failed job for mailcow.local is expected and healthy +if [[ "${FAILED_SYNCS}" != 0 ]] && [[ "${FAILED_SYNCS}" != 1 ]]; then + printf "Dovecot replicator has %d failed jobs\n" "${FAILED_SYNCS}" + ${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH "${FAILED_SYNCS}" > /dev/null +else + ${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null +fi diff --git a/mailcow/data/Dockerfiles/dovecot/report-ham.sieve b/mailcow/data/Dockerfiles/dovecot/report-ham.sieve new file mode 100644 index 0000000..80c7f44 --- /dev/null +++ b/mailcow/data/Dockerfiles/dovecot/report-ham.sieve @@ -0,0 +1,11 @@ +require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"]; + +if environment :matches "imap.mailbox" "*" { + set "mailbox" "${1}"; +} + +if string "${mailbox}" "Trash" { + stop; +} + +pipe :copy "rspamd-pipe-ham"; diff --git a/mailcow/data/Dockerfiles/dovecot/report-spam.sieve b/mailcow/data/Dockerfiles/dovecot/report-spam.sieve new file mode 100644 index 0000000..d44cb9a --- /dev/null +++ b/mailcow/data/Dockerfiles/dovecot/report-spam.sieve @@ -0,0 +1,3 @@ +require ["vnd.dovecot.pipe", "copy"]; + +pipe :copy "rspamd-pipe-spam"; diff --git a/mailcow/data/Dockerfiles/dovecot/rspamd-pipe-ham b/mailcow/data/Dockerfiles/dovecot/rspamd-pipe-ham new file mode 100755 index 0000000..b9a84f1 --- /dev/null +++ b/mailcow/data/Dockerfiles/dovecot/rspamd-pipe-ham @@ -0,0 +1,10 @@ +#!/bin/bash +FILE=/tmp/mail$$ +cat > $FILE +trap "/bin/rm -f $FILE" 0 1 2 3 13 15 + +cat ${FILE} | /usr/bin/curl -H "Flag: 11" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/fuzzydel +cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/learnham +cat ${FILE} | /usr/bin/curl -H "Flag: 13" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/fuzzyadd + +exit 0 diff --git a/mailcow/data/Dockerfiles/dovecot/rspamd-pipe-spam b/mailcow/data/Dockerfiles/dovecot/rspamd-pipe-spam new file mode 100755 index 0000000..3f02c48 --- /dev/null +++ b/mailcow/data/Dockerfiles/dovecot/rspamd-pipe-spam @@ -0,0 +1,10 @@ +#!/bin/bash +FILE=/tmp/mail$$ +cat > $FILE +trap "/bin/rm -f $FILE" 0 1 2 3 13 15 + +cat ${FILE} | /usr/bin/curl -H "Flag: 13" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/fuzzydel +cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/learnspam +cat ${FILE} | /usr/bin/curl -H "Flag: 11" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/fuzzyadd + +exit 0 diff --git a/mailcow/data/Dockerfiles/dovecot/sa-rules.sh b/mailcow/data/Dockerfiles/dovecot/sa-rules.sh new file mode 100755 index 0000000..e948d43 --- /dev/null +++ b/mailcow/data/Dockerfiles/dovecot/sa-rules.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Create temp directories +[[ ! -d /tmp/sa-rules-heinlein ]] && mkdir -p /tmp/sa-rules-heinlein + +# Hash current SA rules +if [[ ! -f /etc/rspamd/custom/sa-rules ]]; then + HASH_SA_RULES=0 +else + HASH_SA_RULES=$(cat /etc/rspamd/custom/sa-rules | md5sum | cut -d' ' -f1) +fi + +# Deploy +if curl --connect-timeout 15 --retry 5 --max-time 30 https://www.spamassassin.heinlein-support.de/$(dig txt 1.4.3.spamassassin.heinlein-support.de +short | tr -d '"' | tr -dc '0-9').tar.gz --output /tmp/sa-rules-heinlein.tar.gz; then + if gzip -t /tmp/sa-rules-heinlein.tar.gz; then + tar xfvz /tmp/sa-rules-heinlein.tar.gz -C /tmp/sa-rules-heinlein + cat /tmp/sa-rules-heinlein/*cf > /etc/rspamd/custom/sa-rules + fi +else + echo "Failed to download SA rules. Exiting." + exit 0 # Must be 0 otherwise dovecot would not start at all +fi + +sed -i -e 's/\([^\\]\)\$\([^\/]\)/\1\\$\2/g' /etc/rspamd/custom/sa-rules + +if [[ "$(cat /etc/rspamd/custom/sa-rules | md5sum | cut -d' ' -f1)" != "${HASH_SA_RULES}" ]]; then + CONTAINER_NAME=rspamd-mailcow + CONTAINER_ID=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | \ + jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | \ + jq -rc "select( .name | tostring | contains(\"${CONTAINER_NAME}\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id") + if [[ ! -z ${CONTAINER_ID} ]]; then + curl --silent --insecure -XPOST --connect-timeout 15 --max-time 120 https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/restart + fi +fi + +# Cleanup +rm -rf /tmp/sa-rules-heinlein /tmp/sa-rules-heinlein.tar.gz diff --git a/mailcow/data/Dockerfiles/dovecot/stop-supervisor.sh b/mailcow/data/Dockerfiles/dovecot/stop-supervisor.sh new file mode 100755 index 0000000..5394490 --- /dev/null +++ b/mailcow/data/Dockerfiles/dovecot/stop-supervisor.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +printf "READY\n"; + +while read line; do + echo "Processing Event: $line" >&2; + kill -3 $(cat "/var/run/supervisord.pid") +done < /dev/stdin diff --git a/mailcow/data/Dockerfiles/dovecot/supervisord.conf b/mailcow/data/Dockerfiles/dovecot/supervisord.conf new file mode 100644 index 0000000..5b00500 --- /dev/null +++ b/mailcow/data/Dockerfiles/dovecot/supervisord.conf @@ -0,0 +1,24 @@ +[supervisord] +nodaemon=true +user=root +pidfile=/var/run/supervisord.pid + +[program:syslog-ng] +command=/usr/sbin/syslog-ng --foreground --no-caps +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autostart=true + +[program:dovecot] +command=/usr/sbin/dovecot -F +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autorestart=true + +[eventlistener:processes] +command=/usr/local/sbin/stop-supervisor.sh +events=PROCESS_STATE_STOPPED, PROCESS_STATE_EXITED, PROCESS_STATE_FATAL diff --git a/mailcow/data/Dockerfiles/dovecot/syslog-ng-redis_slave.conf b/mailcow/data/Dockerfiles/dovecot/syslog-ng-redis_slave.conf new file mode 100644 index 0000000..c028bcd --- /dev/null +++ b/mailcow/data/Dockerfiles/dovecot/syslog-ng-redis_slave.conf @@ -0,0 +1,53 @@ +@version: 4.5 +@include "scl.conf" +options { + chain_hostnames(off); + flush_lines(0); + use_dns(no); + use_fqdn(no); + owner("root"); group("adm"); perm(0640); + stats(freq(0)); + keep_timestamp(no); + bad_hostname("^gconfd$"); +}; +source s_dgram { + unix-dgram("/dev/log"); + internal(); +}; +destination d_stdout { pipe("/dev/stdout"); }; +destination d_redis_ui_log { + redis( + host("`REDIS_SLAVEOF_IP`") + persist-name("redis1") + port(`REDIS_SLAVEOF_PORT`) + auth("`REDISPASS`") + command("LPUSH" "DOVECOT_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n") + ); +}; +destination d_redis_f2b_channel { + redis( + host("`REDIS_SLAVEOF_IP`") + persist-name("redis2") + port(`REDIS_SLAVEOF_PORT`) + auth("`REDISPASS`") + command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)") + ); +}; +filter f_mail { facility(mail); }; +filter f_replica { + not match("User has no mail_replica in userdb" value("MESSAGE")); + not match("Error: sync: Unknown user in remote" value("MESSAGE")); +}; +filter f_dovecot_auth_try { + not match("- trying the next passdb" value("MESSAGE")) and + not match("- trying the next userdb" value("MESSAGE")); +}; +log { + source(s_dgram); + filter(f_dovecot_auth_try); + filter(f_replica); + destination(d_stdout); + filter(f_mail); + destination(d_redis_ui_log); + destination(d_redis_f2b_channel); +}; diff --git a/mailcow/data/Dockerfiles/dovecot/syslog-ng.conf b/mailcow/data/Dockerfiles/dovecot/syslog-ng.conf new file mode 100644 index 0000000..1918f4a --- /dev/null +++ b/mailcow/data/Dockerfiles/dovecot/syslog-ng.conf @@ -0,0 +1,53 @@ +@version: 4.5 +@include "scl.conf" +options { + chain_hostnames(off); + flush_lines(0); + use_dns(no); + use_fqdn(no); + owner("root"); group("adm"); perm(0640); + stats(freq(0)); + keep_timestamp(no); + bad_hostname("^gconfd$"); +}; +source s_dgram { + unix-dgram("/dev/log"); + internal(); +}; +destination d_stdout { pipe("/dev/stdout"); }; +destination d_redis_ui_log { + redis( + host("redis-mailcow") + persist-name("redis1") + port(6379) + auth("`REDISPASS`") + command("LPUSH" "DOVECOT_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n") + ); +}; +destination d_redis_f2b_channel { + redis( + host("redis-mailcow") + persist-name("redis2") + port(6379) + auth("`REDISPASS`") + command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)") + ); +}; +filter f_mail { facility(mail); }; +filter f_replica { + not match("User has no mail_replica in userdb" value("MESSAGE")); + not match("Error: sync: Unknown user in remote" value("MESSAGE")); +}; +filter f_dovecot_auth_try { + not match("- trying the next passdb" value("MESSAGE")) and + not match("- trying the next userdb" value("MESSAGE")); +}; +log { + source(s_dgram); + filter(f_dovecot_auth_try); + filter(f_replica); + destination(d_stdout); + filter(f_mail); + destination(d_redis_ui_log); + destination(d_redis_f2b_channel); +}; diff --git a/mailcow/data/Dockerfiles/dovecot/trim_logs.sh b/mailcow/data/Dockerfiles/dovecot/trim_logs.sh new file mode 100755 index 0000000..3b5e052 --- /dev/null +++ b/mailcow/data/Dockerfiles/dovecot/trim_logs.sh @@ -0,0 +1,26 @@ +#!/bin/bash +catch_non_zero() { + CMD=${1} + ${CMD} > /dev/null + EC=$? + if [ ${EC} -ne 0 ]; then + echo "Command ${CMD} failed to execute, exit code was ${EC}" + fi +} +source /source_env.sh +# Do not attempt to write to slave +if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then + REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning" +else + REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning" +fi +catch_non_zero "${REDIS_CMDLINE} LTRIM ACME_LOG 0 ${LOG_LINES}" +catch_non_zero "${REDIS_CMDLINE} LTRIM POSTFIX_MAILLOG 0 ${LOG_LINES}" +catch_non_zero "${REDIS_CMDLINE} LTRIM DOVECOT_MAILLOG 0 ${LOG_LINES}" +catch_non_zero "${REDIS_CMDLINE} LTRIM SOGO_LOG 0 ${LOG_LINES}" +catch_non_zero "${REDIS_CMDLINE} LTRIM NETFILTER_LOG 0 ${LOG_LINES}" +catch_non_zero "${REDIS_CMDLINE} LTRIM AUTODISCOVER_LOG 0 ${LOG_LINES}" +catch_non_zero "${REDIS_CMDLINE} LTRIM API_LOG 0 ${LOG_LINES}" +catch_non_zero "${REDIS_CMDLINE} LTRIM RL_LOG 0 ${LOG_LINES}" +catch_non_zero "${REDIS_CMDLINE} LTRIM WATCHDOG_LOG 0 ${LOG_LINES}" +catch_non_zero "${REDIS_CMDLINE} LTRIM CRON_LOG 0 ${LOG_LINES}" diff --git a/mailcow/data/Dockerfiles/netfilter/Dockerfile b/mailcow/data/Dockerfiles/netfilter/Dockerfile new file mode 100644 index 0000000..57dbd6e --- /dev/null +++ b/mailcow/data/Dockerfiles/netfilter/Dockerfile @@ -0,0 +1,43 @@ +FROM alpine:3.21 + +LABEL maintainer = "The Infrastructure Company GmbH " + +WORKDIR /app + +ARG PIP_BREAK_SYSTEM_PACKAGES=1 +ENV XTABLES_LIBDIR /usr/lib/xtables +ENV PYTHON_IPTABLES_XTABLES_VERSION 12 +ENV IPTABLES_LIBDIR /usr/lib + +RUN apk add --virtual .build-deps \ + gcc \ + python3-dev \ + libffi-dev \ + openssl-dev \ +&& apk add -U python3 \ + iptables \ + iptables-dev \ + ip6tables \ + xtables-addons \ + nftables \ + tzdata \ + py3-pip \ + py3-nftables \ + musl-dev \ +&& pip3 install --ignore-installed --upgrade pip \ + jsonschema \ + python-iptables \ + redis \ + ipaddress \ + dnspython \ +&& apk del .build-deps + +# && pip3 install --upgrade pip python-iptables==0.13.0 redis ipaddress dnspython \ + +COPY modules /app/modules +COPY main.py /app/ +COPY ./docker-entrypoint.sh /app/ + +RUN chmod +x /app/docker-entrypoint.sh + +CMD ["/bin/sh", "-c", "/app/docker-entrypoint.sh"] \ No newline at end of file diff --git a/mailcow/data/Dockerfiles/netfilter/docker-entrypoint.sh b/mailcow/data/Dockerfiles/netfilter/docker-entrypoint.sh new file mode 100755 index 0000000..47370a1 --- /dev/null +++ b/mailcow/data/Dockerfiles/netfilter/docker-entrypoint.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +backend=iptables + +nft list table ip filter &>/dev/null +nftables_found=$? + +iptables -L -n &>/dev/null +iptables_found=$? + +if [ $nftables_found -lt $iptables_found ]; then + backend=nftables +fi + +if [ $nftables_found -gt $iptables_found ]; then + backend=iptables +fi + +if [ $nftables_found -eq 0 ] && [ $nftables_found -eq $iptables_found ]; then + nftables_lines=$(nft list ruleset | wc -l) + iptables_lines=$(iptables-save | wc -l) + if [ $nftables_lines -gt $iptables_lines ]; then + backend=nftables + else + backend=iptables + fi +fi + +exec python -u /app/main.py $backend diff --git a/mailcow/data/Dockerfiles/netfilter/main.py b/mailcow/data/Dockerfiles/netfilter/main.py new file mode 100644 index 0000000..2b332d2 --- /dev/null +++ b/mailcow/data/Dockerfiles/netfilter/main.py @@ -0,0 +1,502 @@ +#!/usr/bin/env python3 + +import re +import os +import sys +import time +import atexit +import signal +import ipaddress +from collections import Counter +from random import randint +from threading import Thread +from threading import Lock +import redis +import json +import dns.resolver +import dns.exception +import uuid +from modules.Logger import Logger +from modules.IPTables import IPTables +from modules.NFTables import NFTables + + +# globals +WHITELIST = [] +BLACKLIST= [] +bans = {} +quit_now = False +exit_code = 0 +lock = Lock() +chain_name = "MAILCOW" +r = None +pubsub = None +clear_before_quit = False + + +def refreshF2boptions(): + global f2boptions + global quit_now + global exit_code + + f2boptions = {} + + if not r.get('F2B_OPTIONS'): + f2boptions['ban_time'] = r.get('F2B_BAN_TIME') + f2boptions['max_ban_time'] = r.get('F2B_MAX_BAN_TIME') + f2boptions['ban_time_increment'] = r.get('F2B_BAN_TIME_INCREMENT') + f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS') + f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW') + f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4') + f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6') + else: + try: + f2boptions = json.loads(r.get('F2B_OPTIONS')) + except ValueError: + logger.logCrit('Error loading F2B options: F2B_OPTIONS is not json') + quit_now = True + exit_code = 2 + + verifyF2boptions(f2boptions) + r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False)) + +def verifyF2boptions(f2boptions): + verifyF2boption(f2boptions,'ban_time', 1800) + verifyF2boption(f2boptions,'max_ban_time', 10000) + verifyF2boption(f2boptions,'ban_time_increment', True) + verifyF2boption(f2boptions,'max_attempts', 10) + verifyF2boption(f2boptions,'retry_window', 600) + verifyF2boption(f2boptions,'netban_ipv4', 32) + verifyF2boption(f2boptions,'netban_ipv6', 128) + verifyF2boption(f2boptions,'banlist_id', str(uuid.uuid4())) + verifyF2boption(f2boptions,'manage_external', 0) + +def verifyF2boption(f2boptions, f2boption, f2bdefault): + f2boptions[f2boption] = f2boptions[f2boption] if f2boption in f2boptions and f2boptions[f2boption] is not None else f2bdefault + +def refreshF2bregex(): + global f2bregex + global quit_now + global exit_code + if not r.get('F2B_REGEX'): + f2bregex = {} + f2bregex[1] = r'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)' + f2bregex[2] = r'Rspamd UI: Invalid password by ([0-9a-f\.:]+)' + f2bregex[3] = r'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+' + f2bregex[4] = r'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+' + f2bregex[5] = r'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+' + f2bregex[6] = r'\w+\([^,]+,([0-9a-f\.:]+),<[^>]+>\): Password mismatch \(SHA1 of given password: [a-f0-9]+\)' + f2bregex[7] = r'\w+\([^,]+,([0-9a-f\.:]+),<[^>]+>\): unknown user \(SHA1 of given password: [a-f0-9]+\)' + f2bregex[8] = r'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked' + f2bregex[9] = r'([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+' + r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False)) + else: + try: + f2bregex = {} + f2bregex = json.loads(r.get('F2B_REGEX')) + except ValueError: + logger.logCrit('Error loading F2B options: F2B_REGEX is not json') + quit_now = True + exit_code = 2 + +def get_ip(address): + ip = ipaddress.ip_address(address) + if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped: + ip = ip.ipv4_mapped + if ip.is_private or ip.is_loopback: + return False + + return ip + +def ban(address): + global f2boptions + global lock + + refreshF2boptions() + MAX_ATTEMPTS = int(f2boptions['max_attempts']) + RETRY_WINDOW = int(f2boptions['retry_window']) + NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4']) + NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6']) + + ip = get_ip(address) + if not ip: return + address = str(ip) + self_network = ipaddress.ip_network(address) + + with lock: + temp_whitelist = set(WHITELIST) + if temp_whitelist: + for wl_key in temp_whitelist: + wl_net = ipaddress.ip_network(wl_key, False) + if wl_net.overlaps(self_network): + logger.logInfo('Address %s is whitelisted by rule %s' % (self_network, wl_net)) + return + + net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False) + net = str(net) + + if not net in bans: + bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0} + + current_attempt = time.time() + if current_attempt - bans[net]['last_attempt'] > RETRY_WINDOW: + bans[net]['attempts'] = 0 + + bans[net]['attempts'] += 1 + bans[net]['last_attempt'] = current_attempt + + if bans[net]['attempts'] >= MAX_ATTEMPTS: + cur_time = int(round(time.time())) + NET_BAN_TIME = calcNetBanTime(bans[net]['ban_counter']) + logger.logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 )) + if type(ip) is ipaddress.IPv4Address and int(f2boptions['manage_external']) != 1: + with lock: + tables.banIPv4(net) + elif int(f2boptions['manage_external']) != 1: + with lock: + tables.banIPv6(net) + + r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME) + else: + logger.logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)) + +def unban(net): + global lock + + if not net in bans: + logger.logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net) + r.hdel('F2B_QUEUE_UNBAN', '%s' % net) + return + + logger.logInfo('Unbanning %s' % net) + if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network: + with lock: + tables.unbanIPv4(net) + else: + with lock: + tables.unbanIPv6(net) + + r.hdel('F2B_ACTIVE_BANS', '%s' % net) + r.hdel('F2B_QUEUE_UNBAN', '%s' % net) + if net in bans: + bans[net]['attempts'] = 0 + bans[net]['ban_counter'] += 1 + +def permBan(net, unban=False): + global f2boptions + global lock + + is_unbanned = False + is_banned = False + if type(ipaddress.ip_network(net, strict=False)) is ipaddress.IPv4Network: + with lock: + if unban: + is_unbanned = tables.unbanIPv4(net) + elif int(f2boptions['manage_external']) != 1: + is_banned = tables.banIPv4(net) + else: + with lock: + if unban: + is_unbanned = tables.unbanIPv6(net) + elif int(f2boptions['manage_external']) != 1: + is_banned = tables.banIPv6(net) + + + if is_unbanned: + r.hdel('F2B_PERM_BANS', '%s' % net) + logger.logCrit('Removed host/network %s from blacklist' % net) + elif is_banned: + r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time()))) + logger.logCrit('Added host/network %s to blacklist' % net) + +def clear(): + global lock + logger.logInfo('Clearing all bans') + for net in bans.copy(): + unban(net) + with lock: + tables.clearIPv4Table() + tables.clearIPv6Table() + try: + if r is not None: + r.delete('F2B_ACTIVE_BANS') + r.delete('F2B_PERM_BANS') + except Exception as ex: + logger.logWarn('Error clearing redis keys F2B_ACTIVE_BANS and F2B_PERM_BANS: %s' % ex) + +def watch(): + global pubsub + global quit_now + global exit_code + + logger.logInfo('Watching Redis channel F2B_CHANNEL') + pubsub.subscribe('F2B_CHANNEL') + + while not quit_now: + try: + for item in pubsub.listen(): + refreshF2bregex() + for rule_id, rule_regex in f2bregex.items(): + if item['data'] and item['type'] == 'message': + try: + result = re.search(rule_regex, item['data']) + except re.error: + result = False + if result: + addr = result.group(1) + ip = ipaddress.ip_address(addr) + if ip.is_private or ip.is_loopback: + continue + logger.logWarn('%s matched rule id %s (%s)' % (addr, rule_id, item['data'])) + ban(addr) + except Exception as ex: + logger.logWarn('Error reading log line from pubsub: %s' % ex) + pubsub = None + quit_now = True + exit_code = 2 + +def snat4(snat_target): + global lock + global quit_now + + while not quit_now: + time.sleep(10) + with lock: + tables.snat4(snat_target, os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24') + +def snat6(snat_target): + global lock + global quit_now + + while not quit_now: + time.sleep(10) + with lock: + tables.snat6(snat_target, os.getenv('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64')) + +def autopurge(): + global f2boptions + + while not quit_now: + time.sleep(10) + refreshF2boptions() + MAX_ATTEMPTS = int(f2boptions['max_attempts']) + QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN') + if QUEUE_UNBAN: + for net in QUEUE_UNBAN: + unban(str(net)) + for net in bans.copy(): + if bans[net]['attempts'] >= MAX_ATTEMPTS: + NET_BAN_TIME = calcNetBanTime(bans[net]['ban_counter']) + TIME_SINCE_LAST_ATTEMPT = time.time() - bans[net]['last_attempt'] + if TIME_SINCE_LAST_ATTEMPT > NET_BAN_TIME: + unban(net) + +def mailcowChainOrder(): + global lock + global quit_now + global exit_code + while not quit_now: + time.sleep(10) + with lock: + quit_now, exit_code = tables.checkIPv4ChainOrder() + if quit_now: return + quit_now, exit_code = tables.checkIPv6ChainOrder() + +def calcNetBanTime(ban_counter): + global f2boptions + + BAN_TIME = int(f2boptions['ban_time']) + MAX_BAN_TIME = int(f2boptions['max_ban_time']) + BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment']) + NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** ban_counter + NET_BAN_TIME = max([BAN_TIME, min([NET_BAN_TIME, MAX_BAN_TIME])]) + return NET_BAN_TIME + +def isIpNetwork(address): + try: + ipaddress.ip_network(address, False) + except ValueError: + return False + return True + +def genNetworkList(list): + resolver = dns.resolver.Resolver() + hostnames = [] + networks = [] + for key in list: + if isIpNetwork(key): + networks.append(key) + else: + hostnames.append(key) + for hostname in hostnames: + hostname_ips = [] + for rdtype in ['A', 'AAAA']: + try: + answer = resolver.resolve(qname=hostname, rdtype=rdtype, lifetime=3) + except dns.exception.Timeout: + logger.logInfo('Hostname %s timedout on resolve' % hostname) + break + except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): + continue + except dns.exception.DNSException as dnsexception: + logger.logInfo('%s' % dnsexception) + continue + for rdata in answer: + hostname_ips.append(rdata.to_text()) + networks.extend(hostname_ips) + return set(networks) + +def whitelistUpdate(): + global lock + global quit_now + global WHITELIST + while not quit_now: + start_time = time.time() + list = r.hgetall('F2B_WHITELIST') + new_whitelist = [] + if list: + new_whitelist = genNetworkList(list) + with lock: + if Counter(new_whitelist) != Counter(WHITELIST): + WHITELIST = new_whitelist + logger.logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST)) + time.sleep(60.0 - ((time.time() - start_time) % 60.0)) + +def blacklistUpdate(): + global quit_now + global BLACKLIST + while not quit_now: + start_time = time.time() + list = r.hgetall('F2B_BLACKLIST') + new_blacklist = [] + if list: + new_blacklist = genNetworkList(list) + if Counter(new_blacklist) != Counter(BLACKLIST): + addban = set(new_blacklist).difference(BLACKLIST) + delban = set(BLACKLIST).difference(new_blacklist) + BLACKLIST = new_blacklist + logger.logInfo('Blacklist was changed, it has %s entries' % len(BLACKLIST)) + if addban: + for net in addban: + permBan(net=net) + if delban: + for net in delban: + permBan(net=net, unban=True) + time.sleep(60.0 - ((time.time() - start_time) % 60.0)) + +def sigterm_quit(signum, frame): + global clear_before_quit + clear_before_quit = True + sys.exit(exit_code) + +def berfore_quit(): + if clear_before_quit: + clear() + if pubsub is not None: + pubsub.unsubscribe() + + +if __name__ == '__main__': + atexit.register(berfore_quit) + signal.signal(signal.SIGTERM, sigterm_quit) + + # init Logger + logger = Logger() + + # init backend + backend = sys.argv[1] + if backend == "nftables": + logger.logInfo('Using NFTables backend') + tables = NFTables(chain_name, logger) + else: + logger.logInfo('Using IPTables backend') + tables = IPTables(chain_name, logger) + + # In case a previous session was killed without cleanup + clear() + + # Reinit MAILCOW chain + # Is called before threads start, no locking + logger.logInfo("Initializing mailcow netfilter chain") + tables.initChainIPv4() + tables.initChainIPv6() + + if os.getenv("DISABLE_NETFILTER_ISOLATION_RULE").lower() in ("y", "yes"): + logger.logInfo(f"Skipping {chain_name} isolation") + else: + logger.logInfo(f"Setting {chain_name} isolation") + tables.create_mailcow_isolation_rule("br-mailcow", [3306, 6379, 8983, 12345], os.getenv("MAILCOW_REPLICA_IP")) + + # connect to redis + while True: + try: + redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '') + redis_slaveof_port = os.getenv('REDIS_SLAVEOF_PORT', '') + if "".__eq__(redis_slaveof_ip): + r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0, password=os.environ['REDISPASS']) + else: + r = redis.StrictRedis(host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0, password=os.environ['REDISPASS']) + r.ping() + pubsub = r.pubsub() + except Exception as ex: + print('%s - trying again in 3 seconds' % (ex)) + time.sleep(3) + else: + break + logger.set_redis(r) + + # rename fail2ban to netfilter + if r.exists('F2B_LOG'): + r.rename('F2B_LOG', 'NETFILTER_LOG') + # clear bans in redis + r.delete('F2B_ACTIVE_BANS') + r.delete('F2B_PERM_BANS') + + refreshF2boptions() + + watch_thread = Thread(target=watch) + watch_thread.daemon = True + watch_thread.start() + + if os.getenv('SNAT_TO_SOURCE') and os.getenv('SNAT_TO_SOURCE') != 'n': + try: + snat_ip = os.getenv('SNAT_TO_SOURCE') + snat_ipo = ipaddress.ip_address(snat_ip) + if type(snat_ipo) is ipaddress.IPv4Address: + snat4_thread = Thread(target=snat4,args=(snat_ip,)) + snat4_thread.daemon = True + snat4_thread.start() + except ValueError: + print(os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address') + + if os.getenv('SNAT6_TO_SOURCE') and os.getenv('SNAT6_TO_SOURCE') != 'n': + try: + snat_ip = os.getenv('SNAT6_TO_SOURCE') + snat_ipo = ipaddress.ip_address(snat_ip) + if type(snat_ipo) is ipaddress.IPv6Address: + snat6_thread = Thread(target=snat6,args=(snat_ip,)) + snat6_thread.daemon = True + snat6_thread.start() + except ValueError: + print(os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address') + + autopurge_thread = Thread(target=autopurge) + autopurge_thread.daemon = True + autopurge_thread.start() + + mailcowchainwatch_thread = Thread(target=mailcowChainOrder) + mailcowchainwatch_thread.daemon = True + mailcowchainwatch_thread.start() + + blacklistupdate_thread = Thread(target=blacklistUpdate) + blacklistupdate_thread.daemon = True + blacklistupdate_thread.start() + + whitelistupdate_thread = Thread(target=whitelistUpdate) + whitelistupdate_thread.daemon = True + whitelistupdate_thread.start() + + while not quit_now: + time.sleep(0.5) + + sys.exit(exit_code) diff --git a/mailcow/data/Dockerfiles/netfilter/modules/IPTables.py b/mailcow/data/Dockerfiles/netfilter/modules/IPTables.py new file mode 100644 index 0000000..3d3d439 --- /dev/null +++ b/mailcow/data/Dockerfiles/netfilter/modules/IPTables.py @@ -0,0 +1,252 @@ +import iptc +import time +import os + +class IPTables: + def __init__(self, chain_name, logger): + self.chain_name = chain_name + self.logger = logger + + def initChainIPv4(self): + if not iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name) in iptc.Table(iptc.Table.FILTER).chains: + iptc.Table(iptc.Table.FILTER).create_chain(self.chain_name) + for c in ['FORWARD', 'INPUT']: + chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c) + rule = iptc.Rule() + rule.src = '0.0.0.0/0' + rule.dst = '0.0.0.0/0' + target = iptc.Target(rule, self.chain_name) + rule.target = target + if rule not in chain.rules: + chain.insert_rule(rule) + + def initChainIPv6(self): + if not iptc.Chain(iptc.Table6(iptc.Table6.FILTER), self.chain_name) in iptc.Table6(iptc.Table6.FILTER).chains: + iptc.Table6(iptc.Table6.FILTER).create_chain(self.chain_name) + for c in ['FORWARD', 'INPUT']: + chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c) + rule = iptc.Rule6() + rule.src = '::/0' + rule.dst = '::/0' + target = iptc.Target(rule, self.chain_name) + rule.target = target + if rule not in chain.rules: + chain.insert_rule(rule) + + def checkIPv4ChainOrder(self): + filter_table = iptc.Table(iptc.Table.FILTER) + filter_table.refresh() + return self.checkChainOrder(filter_table) + + def checkIPv6ChainOrder(self): + filter_table = iptc.Table6(iptc.Table6.FILTER) + filter_table.refresh() + return self.checkChainOrder(filter_table) + + def checkChainOrder(self, filter_table): + err = False + exit_code = None + + forward_chain = iptc.Chain(filter_table, 'FORWARD') + input_chain = iptc.Chain(filter_table, 'INPUT') + for chain in [forward_chain, input_chain]: + target_found = False + for position, item in enumerate(chain.rules): + if item.target.name == self.chain_name: + target_found = True + if position > 2: + self.logger.logCrit('Error in %s chain: %s target not found, restarting container' % (chain.name, self.chain_name)) + err = True + exit_code = 2 + if not target_found: + self.logger.logCrit('Error in %s chain: %s target not found, restarting container' % (chain.name, self.chain_name)) + err = True + exit_code = 2 + + return err, exit_code + + def clearIPv4Table(self): + self.clearTable(iptc.Table(iptc.Table.FILTER)) + + def clearIPv6Table(self): + self.clearTable(iptc.Table6(iptc.Table6.FILTER)) + + def clearTable(self, filter_table): + filter_table.autocommit = False + forward_chain = iptc.Chain(filter_table, "FORWARD") + input_chain = iptc.Chain(filter_table, "INPUT") + mailcow_chain = iptc.Chain(filter_table, self.chain_name) + if mailcow_chain in filter_table.chains: + for rule in mailcow_chain.rules: + mailcow_chain.delete_rule(rule) + for rule in forward_chain.rules: + if rule.target.name == self.chain_name: + forward_chain.delete_rule(rule) + for rule in input_chain.rules: + if rule.target.name == self.chain_name: + input_chain.delete_rule(rule) + filter_table.delete_chain(self.chain_name) + filter_table.commit() + filter_table.refresh() + filter_table.autocommit = True + + def banIPv4(self, source): + chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name) + rule = iptc.Rule() + rule.src = source + target = iptc.Target(rule, "REJECT") + rule.target = target + if rule in chain.rules: + return False + chain.insert_rule(rule) + return True + + def banIPv6(self, source): + chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), self.chain_name) + rule = iptc.Rule6() + rule.src = source + target = iptc.Target(rule, "REJECT") + rule.target = target + if rule in chain.rules: + return False + chain.insert_rule(rule) + return True + + def unbanIPv4(self, source): + chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name) + rule = iptc.Rule() + rule.src = source + target = iptc.Target(rule, "REJECT") + rule.target = target + if rule not in chain.rules: + return False + chain.delete_rule(rule) + return True + + def unbanIPv6(self, source): + chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), self.chain_name) + rule = iptc.Rule6() + rule.src = source + target = iptc.Target(rule, "REJECT") + rule.target = target + if rule not in chain.rules: + return False + chain.delete_rule(rule) + return True + + def snat4(self, snat_target, source): + try: + table = iptc.Table('nat') + table.refresh() + chain = iptc.Chain(table, 'POSTROUTING') + table.autocommit = False + new_rule = self.getSnat4Rule(snat_target, source) + + if not chain.rules: + # if there are no rules in the chain, insert the new rule directly + self.logger.logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}') + chain.insert_rule(new_rule) + else: + for position, rule in enumerate(chain.rules): + if not hasattr(rule.target, 'parameter'): + continue + match = all(( + new_rule.get_src() == rule.get_src(), + new_rule.get_dst() == rule.get_dst(), + new_rule.target.parameters == rule.target.parameters, + new_rule.target.name == rule.target.name + )) + if position == 0: + if not match: + self.logger.logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}') + chain.insert_rule(new_rule) + else: + if match: + self.logger.logInfo(f'Remove rule for source network {new_rule.src} to SNAT target {snat_target} from POSTROUTING chain at position {position}') + chain.delete_rule(rule) + + table.commit() + table.autocommit = True + return True + except: + self.logger.logCrit('Error running SNAT4, retrying...') + return False + + def snat6(self, snat_target, source): + try: + table = iptc.Table6('nat') + table.refresh() + chain = iptc.Chain(table, 'POSTROUTING') + table.autocommit = False + new_rule = self.getSnat6Rule(snat_target, source) + + if new_rule not in chain.rules: + self.logger.logInfo('Added POSTROUTING rule for source network %s to SNAT target %s' % (new_rule.src, snat_target)) + chain.insert_rule(new_rule) + else: + for position, item in enumerate(chain.rules): + if item == new_rule: + if position != 0: + chain.delete_rule(new_rule) + + table.commit() + table.autocommit = True + except: + self.logger.logCrit('Error running SNAT6, retrying...') + + + def getSnat4Rule(self, snat_target, source): + rule = iptc.Rule() + rule.src = source + rule.dst = '!' + rule.src + target = rule.create_target("SNAT") + target.to_source = snat_target + match = rule.create_match("comment") + match.comment = f'{int(round(time.time()))}' + return rule + + def getSnat6Rule(self, snat_target, source): + rule = iptc.Rule6() + rule.src = source + rule.dst = '!' + rule.src + target = rule.create_target("SNAT") + target.to_source = snat_target + return rule + + def create_mailcow_isolation_rule(self, _interface:str, _dports:list, _allow:str = ""): + try: + chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name) + + # insert mailcow isolation rule + rule = iptc.Rule() + rule.in_interface = f'!{_interface}' + rule.out_interface = _interface + rule.protocol = 'tcp' + rule.create_target("DROP") + match = rule.create_match("multiport") + match.dports = ','.join(map(str, _dports)) + + if rule in chain.rules: + chain.delete_rule(rule) + chain.insert_rule(rule, position=0) + + # insert mailcow isolation exception rule + if _allow != "": + rule = iptc.Rule() + rule.src = _allow + rule.in_interface = f'!{_interface}' + rule.out_interface = _interface + rule.protocol = 'tcp' + rule.create_target("ACCEPT") + match = rule.create_match("multiport") + match.dports = ','.join(map(str, _dports)) + + if rule in chain.rules: + chain.delete_rule(rule) + chain.insert_rule(rule, position=0) + + + return True + except Exception as e: + self.logger.logCrit(f"Error adding {self.chain_name} isolation: {e}") + return False \ No newline at end of file diff --git a/mailcow/data/Dockerfiles/netfilter/modules/Logger.py b/mailcow/data/Dockerfiles/netfilter/modules/Logger.py new file mode 100644 index 0000000..0ba2f42 --- /dev/null +++ b/mailcow/data/Dockerfiles/netfilter/modules/Logger.py @@ -0,0 +1,30 @@ +import time +import json + +class Logger: + def __init__(self): + self.r = None + + def set_redis(self, redis): + self.r = redis + + def log(self, priority, message): + tolog = {} + tolog['time'] = int(round(time.time())) + tolog['priority'] = priority + tolog['message'] = message + print(message) + if self.r is not None: + try: + self.r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False)) + except Exception as ex: + print('Failed logging to redis: %s' % (ex)) + + def logWarn(self, message): + self.log('warn', message) + + def logCrit(self, message): + self.log('crit', message) + + def logInfo(self, message): + self.log('info', message) diff --git a/mailcow/data/Dockerfiles/netfilter/modules/NFTables.py b/mailcow/data/Dockerfiles/netfilter/modules/NFTables.py new file mode 100644 index 0000000..7740fa5 --- /dev/null +++ b/mailcow/data/Dockerfiles/netfilter/modules/NFTables.py @@ -0,0 +1,659 @@ +import nftables +import ipaddress +import os + +class NFTables: + def __init__(self, chain_name, logger): + self.chain_name = chain_name + self.logger = logger + + self.nft = nftables.Nftables() + self.nft.set_json_output(True) + self.nft.set_handle_output(True) + self.nft_chain_names = {'ip': {'filter': {'input': '', 'forward': ''}, 'nat': {'postrouting': ''} }, + 'ip6': {'filter': {'input': '', 'forward': ''}, 'nat': {'postrouting': ''} } } + + self.search_current_chains() + + def initChainIPv4(self): + self.insert_mailcow_chains("ip") + + def initChainIPv6(self): + self.insert_mailcow_chains("ip6") + + def checkIPv4ChainOrder(self): + return self.checkChainOrder("ip") + + def checkIPv6ChainOrder(self): + return self.checkChainOrder("ip6") + + def checkChainOrder(self, filter_table): + err = False + exit_code = None + + for chain in ['input', 'forward']: + chain_position = self.check_mailcow_chains(filter_table, chain) + if chain_position is None: continue + + if chain_position is False: + self.logger.logCrit(f'MAILCOW target not found in {filter_table} {chain} table, restarting container to fix it...') + err = True + exit_code = 2 + + if chain_position > 0: + chain_position += 1 + self.logger.logCrit(f'MAILCOW target is in position {chain_position} in the {filter_table} {chain} table, restarting container to fix it...') + err = True + exit_code = 2 + + return err, exit_code + + def clearIPv4Table(self): + self.clearTable("ip") + + def clearIPv6Table(self): + self.clearTable("ip6") + + def clearTable(self, _family): + is_empty_dict = True + json_command = self.get_base_dict() + chain_handle = self.get_chain_handle(_family, "filter", self.chain_name) + # if no handle, the chain doesn't exists + if chain_handle is not None: + is_empty_dict = False + # flush chain + mailcow_chain = {'family': _family, 'table': 'filter', 'name': self.chain_name} + flush_chain = {'flush': {'chain': mailcow_chain}} + json_command["nftables"].append(flush_chain) + + # remove rule in forward chain + # remove rule in input chain + chains_family = [self.nft_chain_names[_family]['filter']['input'], + self.nft_chain_names[_family]['filter']['forward'] ] + + for chain_base in chains_family: + if not chain_base: continue + + rules_handle = self.get_rules_handle(_family, "filter", chain_base) + if rules_handle is not None: + for r_handle in rules_handle: + is_empty_dict = False + mailcow_rule = {'family':_family, + 'table': 'filter', + 'chain': chain_base, + 'handle': r_handle } + delete_rules = {'delete': {'rule': mailcow_rule} } + json_command["nftables"].append(delete_rules) + + # remove chain + # after delete all rules referencing this chain + if chain_handle is not None: + mc_chain_handle = {'family':_family, + 'table': 'filter', + 'name': self.chain_name, + 'handle': chain_handle } + delete_chain = {'delete': {'chain': mc_chain_handle} } + json_command["nftables"].append(delete_chain) + + if is_empty_dict == False: + if self.nft_exec_dict(json_command): + self.logger.logInfo(f"Clear completed: {_family}") + + def banIPv4(self, source): + ban_dict = self.get_ban_ip_dict(source, "ip") + return self.nft_exec_dict(ban_dict) + + def banIPv6(self, source): + ban_dict = self.get_ban_ip_dict(source, "ip6") + return self.nft_exec_dict(ban_dict) + + def unbanIPv4(self, source): + unban_dict = self.get_unban_ip_dict(source, "ip") + if not unban_dict: + return False + return self.nft_exec_dict(unban_dict) + + def unbanIPv6(self, source): + unban_dict = self.get_unban_ip_dict(source, "ip6") + if not unban_dict: + return False + return self.nft_exec_dict(unban_dict) + + def snat4(self, snat_target, source): + self.snat_rule("ip", snat_target, source) + + def snat6(self, snat_target, source): + self.snat_rule("ip6", snat_target, source) + + + def nft_exec_dict(self, query: dict): + if not query: return False + + rc, output, error = self.nft.json_cmd(query) + if rc != 0: + #self.logger.logCrit(f"Nftables Error: {error}") + return False + + # Prevent returning False or empty string on commands that do not produce output + if rc == 0 and len(output) == 0: + return True + + return output + + def get_base_dict(self): + return {'nftables': [{ 'metainfo': { 'json_schema_version': 1} } ] } + + def search_current_chains(self): + nft_chain_priority = {'ip': {'filter': {'input': None, 'forward': None}, 'nat': {'postrouting': None} }, + 'ip6': {'filter': {'input': None, 'forward': None}, 'nat': {'postrouting': None} } } + + # Command: 'nft list chains' + _list = {'list' : {'chains': 'null'} } + command = self.get_base_dict() + command['nftables'].append(_list) + kernel_ruleset = self.nft_exec_dict(command) + if kernel_ruleset: + for _object in kernel_ruleset['nftables']: + chain = _object.get("chain") + if not chain: continue + + _family = chain['family'] + _table = chain['table'] + _hook = chain.get("hook") + _priority = chain.get("prio") + _name = chain['name'] + + if _family not in self.nft_chain_names: continue + if _table not in self.nft_chain_names[_family]: continue + if _hook not in self.nft_chain_names[_family][_table]: continue + if _priority is None: continue + + _saved_priority = nft_chain_priority[_family][_table][_hook] + if _saved_priority is None or _priority < _saved_priority: + # at this point, we know the chain has: + # hook and priority set + # and it has the lowest priority + nft_chain_priority[_family][_table][_hook] = _priority + self.nft_chain_names[_family][_table][_hook] = _name + + def search_for_chain(self, kernel_ruleset: dict, chain_name: str): + found = False + for _object in kernel_ruleset["nftables"]: + chain = _object.get("chain") + if not chain: + continue + ch_name = chain.get("name") + if ch_name == chain_name: + found = True + break + return found + + def get_chain_dict(self, _family: str, _name: str): + # nft (add | create) chain [] + _chain_opts = {'family': _family, 'table': 'filter', 'name': _name } + _add = {'add': {'chain': _chain_opts} } + final_chain = self.get_base_dict() + final_chain["nftables"].append(_add) + return final_chain + + def get_mailcow_jump_rule_dict(self, _family: str, _chain: str): + _jump_rule = self.get_base_dict() + _expr_opt=[] + _expr_counter = {'family': _family, 'table': 'filter', 'packets': 0, 'bytes': 0} + _counter_dict = {'counter': _expr_counter} + _expr_opt.append(_counter_dict) + + _jump_opts = {'jump': {'target': self.chain_name} } + + _expr_opt.append(_jump_opts) + + _rule_params = {'family': _family, + 'table': 'filter', + 'chain': _chain, + 'expr': _expr_opt, + 'comment': "mailcow" } + + _add_rule = {'insert': {'rule': _rule_params} } + + _jump_rule["nftables"].append(_add_rule) + + return _jump_rule + + def insert_mailcow_chains(self, _family: str): + nft_input_chain = self.nft_chain_names[_family]['filter']['input'] + nft_forward_chain = self.nft_chain_names[_family]['filter']['forward'] + # Command: 'nft list table filter' + _table_opts = {'family': _family, 'name': 'filter'} + _list = {'list': {'table': _table_opts} } + command = self.get_base_dict() + command['nftables'].append(_list) + kernel_ruleset = self.nft_exec_dict(command) + if kernel_ruleset: + # chain + if not self.search_for_chain(kernel_ruleset, self.chain_name): + cadena = self.get_chain_dict(_family, self.chain_name) + if self.nft_exec_dict(cadena): + self.logger.logInfo(f"MAILCOW {_family} chain created successfully.") + + input_jump_found, forward_jump_found = False, False + + for _object in kernel_ruleset["nftables"]: + if not _object.get("rule"): + continue + + rule = _object["rule"] + if nft_input_chain and rule["chain"] == nft_input_chain: + if rule.get("comment") and rule["comment"] == "mailcow": + input_jump_found = True + if nft_forward_chain and rule["chain"] == nft_forward_chain: + if rule.get("comment") and rule["comment"] == "mailcow": + forward_jump_found = True + + if not input_jump_found: + command = self.get_mailcow_jump_rule_dict(_family, nft_input_chain) + self.nft_exec_dict(command) + + if not forward_jump_found: + command = self.get_mailcow_jump_rule_dict(_family, nft_forward_chain) + self.nft_exec_dict(command) + + def delete_nat_rule(self, _family:str, _chain: str, _handle:str): + delete_command = self.get_base_dict() + _rule_opts = {'family': _family, + 'table': 'nat', + 'chain': _chain, + 'handle': _handle } + _delete = {'delete': {'rule': _rule_opts} } + delete_command["nftables"].append(_delete) + + return self.nft_exec_dict(delete_command) + + def delete_filter_rule(self, _family:str, _chain: str, _handle:str): + delete_command = self.get_base_dict() + _rule_opts = {'family': _family, + 'table': 'filter', + 'chain': _chain, + 'handle': _handle } + _delete = {'delete': {'rule': _rule_opts} } + delete_command["nftables"].append(_delete) + + return self.nft_exec_dict(delete_command) + + def snat_rule(self, _family: str, snat_target: str, source_address: str): + chain_name = self.nft_chain_names[_family]['nat']['postrouting'] + + # no postrouting chain, may occur if docker has ipv6 disabled. + if not chain_name: return + + # Command: nft list chain nat + _chain_opts = {'family': _family, 'table': 'nat', 'name': chain_name} + _list = {'list':{'chain': _chain_opts} } + command = self.get_base_dict() + command['nftables'].append(_list) + kernel_ruleset = self.nft_exec_dict(command) + if not kernel_ruleset: + return + + rule_position = 0 + rule_handle = None + rule_found = False + for _object in kernel_ruleset["nftables"]: + if not _object.get("rule"): + continue + + rule = _object["rule"] + if not rule.get("comment") or not rule["comment"] == "mailcow": + rule_position +=1 + continue + + rule_found = True + rule_handle = rule["handle"] + break + + dest_net = ipaddress.ip_network(source_address, strict=False) + target_net = ipaddress.ip_network(snat_target, strict=False) + + if rule_found: + saddr_ip = rule["expr"][0]["match"]["right"]["prefix"]["addr"] + saddr_len = int(rule["expr"][0]["match"]["right"]["prefix"]["len"]) + + daddr_ip = rule["expr"][1]["match"]["right"]["prefix"]["addr"] + daddr_len = int(rule["expr"][1]["match"]["right"]["prefix"]["len"]) + + target_ip = rule["expr"][3]["snat"]["addr"] + + saddr_net = ipaddress.ip_network(saddr_ip + '/' + str(saddr_len), strict=False) + daddr_net = ipaddress.ip_network(daddr_ip + '/' + str(daddr_len), strict=False) + current_target_net = ipaddress.ip_network(target_ip, strict=False) + + match = all(( + dest_net == saddr_net, + dest_net == daddr_net, + target_net == current_target_net + )) + try: + if rule_position == 0: + if not match: + # Position 0 , it is a mailcow rule , but it does not have the same parameters + if self.delete_nat_rule(_family, chain_name, rule_handle): + self.logger.logInfo(f'Remove rule for source network {saddr_net} to SNAT target {target_net} from {_family} nat {chain_name} chain, rule does not match configured parameters') + else: + # Position > 0 and is mailcow rule + if self.delete_nat_rule(_family, chain_name, rule_handle): + self.logger.logInfo(f'Remove rule for source network {saddr_net} to SNAT target {target_net} from {_family} nat {chain_name} chain, rule is at position {rule_position}') + except: + self.logger.logCrit(f"Error running SNAT on {_family}, retrying..." ) + else: + # rule not found + json_command = self.get_base_dict() + try: + snat_dict = {'snat': {'addr': str(target_net.network_address)} } + + expr_counter = {'family': _family, 'table': 'nat', 'packets': 0, 'bytes': 0} + counter_dict = {'counter': expr_counter} + + prefix_dict = {'prefix': {'addr': str(dest_net.network_address), 'len': int(dest_net.prefixlen)} } + payload_dict = {'payload': {'protocol': _family, 'field': "saddr"} } + match_dict1 = {'match': {'op': '==', 'left': payload_dict, 'right': prefix_dict} } + + payload_dict2 = {'payload': {'protocol': _family, 'field': "daddr"} } + match_dict2 = {'match': {'op': '!=', 'left': payload_dict2, 'right': prefix_dict } } + expr_list = [ + match_dict1, + match_dict2, + counter_dict, + snat_dict + ] + rule_fields = {'family': _family, + 'table': 'nat', + 'chain': chain_name, + 'comment': "mailcow", + 'expr': expr_list } + + insert_dict = {'insert': {'rule': rule_fields} } + json_command["nftables"].append(insert_dict) + if self.nft_exec_dict(json_command): + self.logger.logInfo(f'Added {_family} nat {chain_name} rule for source network {dest_net} to {target_net}') + except: + self.logger.logCrit(f"Error running SNAT on {_family}, retrying...") + + def get_chain_handle(self, _family: str, _table: str, chain_name: str): + chain_handle = None + # Command: 'nft list chains {family}' + _list = {'list': {'chains': {'family': _family} } } + command = self.get_base_dict() + command['nftables'].append(_list) + kernel_ruleset = self.nft_exec_dict(command) + if kernel_ruleset: + for _object in kernel_ruleset["nftables"]: + if not _object.get("chain"): + continue + chain = _object["chain"] + if chain["family"] == _family and chain["table"] == _table and chain["name"] == chain_name: + chain_handle = chain["handle"] + break + return chain_handle + + def get_rules_handle(self, _family: str, _table: str, chain_name: str, _comment_filter = "mailcow"): + rule_handle = [] + # Command: 'nft list chain {family} {table} {chain_name}' + _chain_opts = {'family': _family, 'table': _table, 'name': chain_name} + _list = {'list': {'chain': _chain_opts} } + command = self.get_base_dict() + command['nftables'].append(_list) + + kernel_ruleset = self.nft_exec_dict(command) + if kernel_ruleset: + for _object in kernel_ruleset["nftables"]: + if not _object.get("rule"): + continue + + rule = _object["rule"] + if rule["family"] == _family and rule["table"] == _table and rule["chain"] == chain_name: + if rule.get("comment") and rule["comment"] == _comment_filter: + rule_handle.append(rule["handle"]) + return rule_handle + + def get_ban_ip_dict(self, ipaddr: str, _family: str): + json_command = self.get_base_dict() + + expr_opt = [] + ipaddr_net = ipaddress.ip_network(ipaddr, strict=False) + right_dict = {'prefix': {'addr': str(ipaddr_net.network_address), 'len': int(ipaddr_net.prefixlen) } } + + left_dict = {'payload': {'protocol': _family, 'field': 'saddr'} } + match_dict = {'op': '==', 'left': left_dict, 'right': right_dict } + expr_opt.append({'match': match_dict}) + + counter_dict = {'counter': {'family': _family, 'table': "filter", 'packets': 0, 'bytes': 0} } + expr_opt.append(counter_dict) + + expr_opt.append({'drop': "null"}) + + rule_dict = {'family': _family, 'table': "filter", 'chain': self.chain_name, 'expr': expr_opt} + + base_dict = {'insert': {'rule': rule_dict} } + json_command["nftables"].append(base_dict) + + return json_command + + def get_unban_ip_dict(self, ipaddr:str, _family: str): + json_command = self.get_base_dict() + # Command: 'nft list chain {s_family} filter MAILCOW' + _chain_opts = {'family': _family, 'table': 'filter', 'name': self.chain_name} + _list = {'list': {'chain': _chain_opts} } + command = self.get_base_dict() + command['nftables'].append(_list) + kernel_ruleset = self.nft_exec_dict(command) + rule_handle = None + if kernel_ruleset: + for _object in kernel_ruleset["nftables"]: + if not _object.get("rule"): + continue + + rule = _object["rule"]["expr"][0]["match"] + if not "payload" in rule["left"]: + continue + left_opt = rule["left"]["payload"] + if not left_opt["protocol"] == _family: + continue + if not left_opt["field"] =="saddr": + continue + + # ip currently banned + rule_right = rule["right"] + if isinstance(rule_right, dict): + current_rule_ip = rule_right["prefix"]["addr"] + '/' + str(rule_right["prefix"]["len"]) + else: + current_rule_ip = rule_right + current_rule_net = ipaddress.ip_network(current_rule_ip) + + # ip to ban + candidate_net = ipaddress.ip_network(ipaddr, strict=False) + + if current_rule_net == candidate_net: + rule_handle = _object["rule"]["handle"] + break + + if rule_handle is not None: + mailcow_rule = {'family': _family, 'table': 'filter', 'chain': self.chain_name, 'handle': rule_handle} + delete_rule = {'delete': {'rule': mailcow_rule} } + json_command["nftables"].append(delete_rule) + else: + return False + + return json_command + + def check_mailcow_chains(self, family: str, chain: str): + position = 0 + rule_found = False + chain_name = self.nft_chain_names[family]['filter'][chain] + + if not chain_name: return None + + _chain_opts = {'family': family, 'table': 'filter', 'name': chain_name} + _list = {'list': {'chain': _chain_opts}} + command = self.get_base_dict() + command['nftables'].append(_list) + kernel_ruleset = self.nft_exec_dict(command) + if kernel_ruleset: + for _object in kernel_ruleset["nftables"]: + if not _object.get("rule"): + continue + rule = _object["rule"] + if rule.get("comment") and rule["comment"] == "mailcow": + rule_found = True + break + + position+=1 + + return position if rule_found else False + + def create_mailcow_isolation_rule(self, _interface:str, _dports:list, _allow:str = ""): + family = "ip" + table = "filter" + comment_filter_drop = "mailcow isolation" + comment_filter_allow = "mailcow isolation allow" + json_command = self.get_base_dict() + + # Delete old mailcow isolation rules + handles = self.get_rules_handle(family, table, self.chain_name, comment_filter_drop) + for handle in handles: + self.delete_filter_rule(family, self.chain_name, handle) + handles = self.get_rules_handle(family, table, self.chain_name, comment_filter_allow) + for handle in handles: + self.delete_filter_rule(family, self.chain_name, handle) + + # insert mailcow isolation rule + _match_dict_drop = [ + { + "match": { + "op": "!=", + "left": { + "meta": { + "key": "iifname" + } + }, + "right": _interface + } + }, + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "oifname" + } + }, + "right": _interface + } + }, + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "tcp", + "field": "dport" + } + }, + "right": { + "set": _dports + } + } + }, + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "drop": None + } + ] + rule_drop = { "insert": { "rule": { + "family": family, + "table": table, + "chain": self.chain_name, + "comment": comment_filter_drop, + "expr": _match_dict_drop + }}} + json_command["nftables"].append(rule_drop) + + # insert mailcow isolation allow rule + if _allow != "": + _match_dict_allow = [ + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "ip", + "field": "saddr" + } + }, + "right": _allow + } + }, + { + "match": { + "op": "!=", + "left": { + "meta": { + "key": "iifname" + } + }, + "right": _interface + } + }, + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "oifname" + } + }, + "right": _interface + } + }, + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "tcp", + "field": "dport" + } + }, + "right": { + "set": _dports + } + } + }, + { + "counter": { + "packets": 0, + "bytes": 0 + } + }, + { + "accept": None + } + ] + rule_allow = { "insert": { "rule": { + "family": family, + "table": table, + "chain": self.chain_name, + "comment": comment_filter_allow, + "expr": _match_dict_allow + }}} + json_command["nftables"].append(rule_allow) + + success = self.nft_exec_dict(json_command) + if success == False: + self.logger.logCrit(f"Error adding {self.chain_name} isolation") + return False + + return True \ No newline at end of file diff --git a/mailcow/data/Dockerfiles/netfilter/modules/__init__.py b/mailcow/data/Dockerfiles/netfilter/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mailcow/data/Dockerfiles/nginx/Dockerfile b/mailcow/data/Dockerfiles/nginx/Dockerfile new file mode 100644 index 0000000..7d2ce34 --- /dev/null +++ b/mailcow/data/Dockerfiles/nginx/Dockerfile @@ -0,0 +1,18 @@ +FROM nginx:alpine +LABEL maintainer "The Infrastructure Company GmbH " + +ENV PIP_BREAK_SYSTEM_PACKAGES=1 + +RUN apk add --no-cache nginx \ + python3 \ + py3-pip && \ + pip install --upgrade pip && \ + pip install Jinja2 + +RUN mkdir -p /etc/nginx/includes + +COPY ./bootstrap.py / +COPY ./docker-entrypoint.sh / + +ENTRYPOINT ["/docker-entrypoint.sh"] +CMD ["nginx", "-g", "daemon off;"] diff --git a/mailcow/data/Dockerfiles/nginx/bootstrap.py b/mailcow/data/Dockerfiles/nginx/bootstrap.py new file mode 100644 index 0000000..11e6fc2 --- /dev/null +++ b/mailcow/data/Dockerfiles/nginx/bootstrap.py @@ -0,0 +1,100 @@ +import os +import subprocess +from jinja2 import Environment, FileSystemLoader + +def includes_conf(env, template_vars): + server_name = "server_name.active" + listen_plain = "listen_plain.active" + listen_ssl = "listen_ssl.active" + + server_name_config = f"server_name {template_vars['MAILCOW_HOSTNAME']} autodiscover.* autoconfig.* {' '.join(template_vars['ADDITIONAL_SERVER_NAMES'])};" + listen_plain_config = f"listen {template_vars['HTTP_PORT']};" + listen_ssl_config = f"listen {template_vars['HTTPS_PORT']};" + if not template_vars['DISABLE_IPv6']: + listen_plain_config += f"\nlisten [::]:{template_vars['HTTP_PORT']};" + listen_ssl_config += f"\nlisten [::]:{template_vars['HTTPS_PORT']} ssl;" + listen_ssl_config += "\nhttp2 on;" + + with open(f"/etc/nginx/conf.d/{server_name}", "w") as f: + f.write(server_name_config) + + with open(f"/etc/nginx/conf.d/{listen_plain}", "w") as f: + f.write(listen_plain_config) + + with open(f"/etc/nginx/conf.d/{listen_ssl}", "w") as f: + f.write(listen_ssl_config) + +def sites_default_conf(env, template_vars): + config_name = "sites-default.conf" + template = env.get_template(f"{config_name}.j2") + config = template.render(template_vars) + + with open(f"/etc/nginx/includes/{config_name}", "w") as f: + f.write(config) + +def nginx_conf(env, template_vars): + config_name = "nginx.conf" + template = env.get_template(f"{config_name}.j2") + config = template.render(template_vars) + + with open(f"/etc/nginx/{config_name}", "w") as f: + f.write(config) + +def prepare_template_vars(): + ipv4_network = os.getenv("IPV4_NETWORK", "172.22.1") + additional_server_names = os.getenv("ADDITIONAL_SERVER_NAMES", "") + trusted_proxies = os.getenv("TRUSTED_PROXIES", "") + + template_vars = { + 'IPV4_NETWORK': ipv4_network, + 'TRUSTED_PROXIES': [item.strip() for item in trusted_proxies.split(",") if item.strip()], + 'SKIP_RSPAMD': os.getenv("SKIP_RSPAMD", "n").lower() in ("y", "yes"), + 'SKIP_SOGO': os.getenv("SKIP_SOGO", "n").lower() in ("y", "yes"), + 'NGINX_USE_PROXY_PROTOCOL': os.getenv("NGINX_USE_PROXY_PROTOCOL", "n").lower() in ("y", "yes"), + 'MAILCOW_HOSTNAME': os.getenv("MAILCOW_HOSTNAME", ""), + 'ADDITIONAL_SERVER_NAMES': [item.strip() for item in additional_server_names.split(",") if item.strip()], + 'HTTP_PORT': os.getenv("HTTP_PORT", "80"), + 'HTTPS_PORT': os.getenv("HTTPS_PORT", "443"), + 'SOGOHOST': os.getenv("SOGOHOST", ipv4_network + ".248"), + 'RSPAMDHOST': os.getenv("RSPAMDHOST", "rspamd-mailcow"), + 'PHPFPMHOST': os.getenv("PHPFPMHOST", "php-fpm-mailcow"), + 'DISABLE_IPv6': os.getenv("DISABLE_IPv6", "n").lower() in ("y", "yes"), + 'HTTP_REDIRECT': os.getenv("HTTP_REDIRECT", "n").lower() in ("y", "yes"), + } + + ssl_dir = '/etc/ssl/mail/' + template_vars['valid_cert_dirs'] = [] + for d in os.listdir(ssl_dir): + full_path = os.path.join(ssl_dir, d) + if not os.path.isdir(full_path): + continue + + cert_path = os.path.join(full_path, 'cert.pem') + key_path = os.path.join(full_path, 'key.pem') + domains_path = os.path.join(full_path, 'domains') + + if os.path.isfile(cert_path) and os.path.isfile(key_path) and os.path.isfile(domains_path): + with open(domains_path, 'r') as file: + domains = file.read().strip() + domains_list = domains.split() + if domains_list and template_vars["MAILCOW_HOSTNAME"] not in domains_list: + template_vars['valid_cert_dirs'].append({ + 'cert_path': full_path + '/', + 'domains': domains + }) + + return template_vars + +def main(): + env = Environment(loader=FileSystemLoader('./etc/nginx/conf.d/templates')) + + # Render config + print("Render config") + template_vars = prepare_template_vars() + sites_default_conf(env, template_vars) + nginx_conf(env, template_vars) + includes_conf(env, template_vars) + + +if __name__ == "__main__": + main() diff --git a/mailcow/data/Dockerfiles/nginx/docker-entrypoint.sh b/mailcow/data/Dockerfiles/nginx/docker-entrypoint.sh new file mode 100755 index 0000000..45e327e --- /dev/null +++ b/mailcow/data/Dockerfiles/nginx/docker-entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +PHPFPMHOST=${PHPFPMHOST:-"php-fpm-mailcow"} +SOGOHOST=${SOGOHOST:-"$IPV4_NETWORK.248"} +RSPAMDHOST=${RSPAMDHOST:-"rspamd-mailcow"} + +until ping ${PHPFPMHOST} -c1 > /dev/null; do + echo "Waiting for PHP..." + sleep 1 +done +if ! printf "%s\n" "${SKIP_SOGO}" | grep -E '^([yY][eE][sS]|[yY])+$' >/dev/null; then + until ping ${SOGOHOST} -c1 > /dev/null; do + echo "Waiting for SOGo..." + sleep 1 + done +fi +if ! printf "%s\n" "${SKIP_RSPAMD}" | grep -E '^([yY][eE][sS]|[yY])+$' >/dev/null; then + until ping ${RSPAMDHOST} -c1 > /dev/null; do + echo "Waiting for Rspamd..." + sleep 1 + done +fi + +python3 /bootstrap.py + +exec "$@" diff --git a/mailcow/data/Dockerfiles/olefy/Dockerfile b/mailcow/data/Dockerfiles/olefy/Dockerfile new file mode 100644 index 0000000..845b125 --- /dev/null +++ b/mailcow/data/Dockerfiles/olefy/Dockerfile @@ -0,0 +1,23 @@ +FROM alpine:3.21 + +LABEL maintainer = "The Infrastructure Company GmbH " + +ARG PIP_BREAK_SYSTEM_PACKAGES=1 +WORKDIR /app + +#RUN addgroup -S olefy && adduser -S olefy -G olefy \ +RUN apk add --virtual .build-deps gcc musl-dev python3-dev libffi-dev openssl-dev cargo \ + && apk add --update --no-cache python3 py3-pip openssl tzdata libmagic \ + && pip3 install --upgrade pip \ + && pip3 install --upgrade asyncio python-magic \ + && pip3 install --upgrade https://github.com/decalage2/oletools/archive/master.zip \ + && apk del .build-deps +# && sed -i 's/template_injection_detected = True/template_injection_detected = False/g' /usr/lib/python3.9/site-packages/oletools/olevba.py + +ADD olefy.py /app/ + +RUN chown -R nobody:nobody /app /tmp + +USER nobody + +CMD ["python3", "-u", "/app/olefy.py"] diff --git a/mailcow/data/Dockerfiles/olefy/olefy.py b/mailcow/data/Dockerfiles/olefy/olefy.py new file mode 100644 index 0000000..776e786 --- /dev/null +++ b/mailcow/data/Dockerfiles/olefy/olefy.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2020, Dennis Kalbhen +# Copyright (c) 2020, Carsten Rosenberg +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +### +# +# olefy is a little helper socket to use oletools with rspamd. (https://rspamd.com) +# Please find updates and issues here: https://github.com/HeinleinSupport/olefy +# +### + +from subprocess import Popen, PIPE +import sys +import os +import logging +import asyncio +import time +import magic +import re + +# merge variables from /etc/olefy.conf and the defaults +olefy_listen_addr_string = os.getenv('OLEFY_BINDADDRESS', '127.0.0.1,::1') +olefy_listen_port = int(os.getenv('OLEFY_BINDPORT', '10050')) +olefy_tmp_dir = os.getenv('OLEFY_TMPDIR', '/tmp') +olefy_python_path = os.getenv('OLEFY_PYTHON_PATH', '/usr/bin/python3') +olefy_olevba_path = os.getenv('OLEFY_OLEVBA_PATH', '/usr/local/bin/olevba3') +# 10:DEBUG, 20:INFO, 30:WARNING, 40:ERROR, 50:CRITICAL +olefy_loglvl = int(os.getenv('OLEFY_LOGLVL', 20)) +olefy_min_length = int(os.getenv('OLEFY_MINLENGTH', 500)) +olefy_del_tmp = int(os.getenv('OLEFY_DEL_TMP', 1)) +olefy_del_tmp_failed = int(os.getenv('OLEFY_DEL_TMP_FAILED', 1)) + +# internal used variables +request_time = '0000000000.000000' +olefy_protocol = 'OLEFY' +olefy_ping = 'PING' +olefy_protocol_sep = '\n\n' +olefy_headers = {} + +# init logging +logger = logging.getLogger('olefy') +logging.basicConfig(stream=sys.stdout, level=olefy_loglvl, format='olefy %(levelname)s %(funcName)s %(message)s') + +logger.debug('olefy listen address string: {} (type {})'.format(olefy_listen_addr_string, type(olefy_listen_addr_string))) + +if not olefy_listen_addr_string: + olefy_listen_addr = "" +else: + addr_re = re.compile('[\[" \]]') + olefy_listen_addr = addr_re.sub('', olefy_listen_addr_string.replace("'", "")).split(',') + +# log runtime variables +logger.info('olefy listen address: {} (type: {})'.format(olefy_listen_addr, type(olefy_listen_addr))) +logger.info('olefy listen port: {}'.format(olefy_listen_port)) +logger.info('olefy tmp dir: {}'.format(olefy_tmp_dir)) +logger.info('olefy python path: {}'.format(olefy_python_path)) +logger.info('olefy olvba path: {}'.format(olefy_olevba_path)) +logger.info('olefy log level: {}'.format(olefy_loglvl)) +logger.info('olefy min file length: {}'.format(olefy_min_length)) +logger.info('olefy delete tmp file: {}'.format(olefy_del_tmp)) +logger.info('olefy delete tmp file when failed: {}'.format(olefy_del_tmp_failed)) + +if not os.path.isfile(olefy_python_path): + logger.critical('python path not found: {}'.format(olefy_python_path)) + exit(1) +if not os.path.isfile(olefy_olevba_path): + logger.critical('olevba path not found: {}'.format(olefy_olevba_path)) + exit(1) + +# olefy protocol function +def protocol_split( olefy_line ): + header_lines = olefy_line.split('\n') + for line in header_lines: + if line == 'OLEFY/1.0': + olefy_headers['olefy'] = line + elif line != '': + kv = line.split(': ') + if kv[0] != '' and kv[1] != '': + olefy_headers[kv[0]] = kv[1] + logger.debug('olefy_headers: {}'.format(olefy_headers)) + +# calling oletools +def oletools( stream, tmp_file_name, lid ): + if olefy_min_length > stream.__len__(): + logger.error('{} {} bytes (Not Scanning! File smaller than {!r})'.format(lid, stream.__len__(), olefy_min_length)) + out = b'[ { "error": "File too small" } ]' + else: + tmp_file = open(tmp_file_name, 'wb') + tmp_file.write(stream) + tmp_file.close() + + file_magic = magic.Magic(mime=True, uncompress=True) + file_mime = file_magic.from_file(tmp_file_name) + logger.info('{} {} (libmagic output)'.format(lid, file_mime)) + + # do the olefy + cmd_tmp = Popen([olefy_python_path, olefy_olevba_path, '-a', '-j' , '-l', 'error', tmp_file_name], stdout=PIPE, stderr=PIPE) + out, err = cmd_tmp.communicate() + out = bytes(out.decode('utf-8', 'ignore').replace(' ', ' ').replace('\t', '').replace('\n', '').replace('XLMMacroDeobfuscator: pywin32 is not installed (only is required if you want to use MS Excel)', ''), encoding="utf-8") + failed = False + if out.__len__() < 30: + logger.error('{} olevba returned <30 chars - rc: {!r}, response: {!r}, error: {!r}'.format(lid,cmd_tmp.returncode, + out.decode('utf-8', 'ignore'), err.decode('utf-8', 'ignore'))) + out = b'[ { "error": "Unhandled error - too short olevba response" } ]' + failed = True + elif err.__len__() > 10 and cmd_tmp.returncode == 9: + logger.error("{} olevba stderr >10 chars - rc: {!r}, response: {!r}".format(lid, cmd_tmp.returncode, err.decode("utf-8", "ignore"))) + out = b'[ { "error": "Decrypt failed" } ]' + failed = True + elif err.__len__() > 10 and cmd_tmp.returncode > 9: + logger.error('{} olevba stderr >10 chars - rc: {!r}, response: {!r}'.format(lid, cmd_tmp.returncode, err.decode('utf-8', 'ignore'))) + out = b'[ { "error": "Unhandled oletools error" } ]' + failed = True + elif cmd_tmp.returncode != 0: + logger.error('{} olevba exited with code {!r}; err: {!r}'.format(lid, cmd_tmp.returncode, err.decode('utf-8', 'ignore'))) + failed = True + + if failed and olefy_del_tmp_failed == 0: + logger.debug('{} {} FAILED: not deleting tmp file'.format(lid, tmp_file_name)) + elif olefy_del_tmp == 1: + logger.debug('{} {} deleting tmp file'.format(lid, tmp_file_name)) + os.remove(tmp_file_name) + + logger.debug('{} response: {}'.format(lid, out.decode('utf-8', 'ignore'))) + return out + b'\t\n\n\t' + +# Asyncio data handling, default AIO-Functions +class AIO(asyncio.Protocol): + def __init__(self): + self.extra = bytearray() + + def connection_made(self, transport): + global request_time + peer = transport.get_extra_info('peername') + logger.debug('{} new connection was made'.format(peer)) + self.transport = transport + request_time = str(time.time()) + + def data_received(self, request, msgid=1): + peer = self.transport.get_extra_info('peername') + logger.debug('{} data received from new connection'.format(peer)) + self.extra.extend(request) + + def eof_received(self): + peer = self.transport.get_extra_info('peername') + olefy_protocol_err = False + proto_ck = self.extra[0:2000].decode('utf-8', 'ignore') + + headers = proto_ck[0:proto_ck.find(olefy_protocol_sep)] + + if olefy_protocol == headers[0:5]: + self.extra = bytearray(self.extra[len(headers)+2:len(self.extra)]) + protocol_split(headers) + else: + olefy_protocol_err = True + + if olefy_ping == headers[0:4]: + is_ping = True + else: + is_ping = False + rspamd_id = olefy_headers['Rspamd-ID'][:6] or '' + lid = 'Rspamd-ID' in olefy_headers and '<'+rspamd_id+'>' + tmp_file_name = olefy_tmp_dir+'/'+request_time+'.'+str(peer[1])+'.'+rspamd_id + logger.debug('{} {} choosen as tmp filename'.format(lid, tmp_file_name)) + + if not is_ping or olefy_loglvl == 10: + logger.info('{} {} bytes (stream size)'.format(lid, self.extra.__len__())) + + if olefy_ping == headers[0:4]: + logger.debug('{} PING request'.format(peer)) + out = b'PONG' + elif olefy_protocol_err == True or olefy_headers['olefy'] != 'OLEFY/1.0': + logger.error('{} Protocol ERROR: no OLEFY/1.0 found'.format(lid)) + out = b'[ { "error": "Protocol error" } ]' + elif 'Method' in olefy_headers: + if olefy_headers['Method'] == 'oletools': + out = oletools(self.extra, tmp_file_name, lid) + else: + logger.error('Protocol ERROR: Method header not found') + out = b'[ { "error": "Protocol error: Method header not found" } ]' + + self.transport.write(out) + if not is_ping or olefy_loglvl == 10: + logger.info('{} {} response send: {!r}'.format(lid, peer, out)) + self.transport.close() + + +# start the listeners +loop = asyncio.get_event_loop() +# each client connection will create a new protocol instance +coro = loop.create_server(AIO, olefy_listen_addr, olefy_listen_port) +server = loop.run_until_complete(coro) +for sockets in server.sockets: + logger.info('serving on {}'.format(sockets.getsockname())) + +# XXX serve requests until KeyboardInterrupt, not needed for production +try: + loop.run_forever() +except KeyboardInterrupt: + pass + +# graceful shutdown/reload +server.close() +loop.run_until_complete(server.wait_closed()) +loop.close() +logger.info('stopped serving') diff --git a/mailcow/data/Dockerfiles/phpfpm/Dockerfile b/mailcow/data/Dockerfiles/phpfpm/Dockerfile new file mode 100644 index 0000000..4ba8349 --- /dev/null +++ b/mailcow/data/Dockerfiles/phpfpm/Dockerfile @@ -0,0 +1,114 @@ +FROM php:8.2-fpm-alpine3.21 + +LABEL maintainer = "The Infrastructure Company GmbH " + +# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?.*)$ +ARG APCU_PECL_VERSION=5.1.24 +# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?.*)$ +ARG IMAGICK_PECL_VERSION=3.7.0 +# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?.*)$ +ARG MAILPARSE_PECL_VERSION=3.1.8 +# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?.*)$ +ARG MEMCACHED_PECL_VERSION=3.2.0 +# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?.*)$ +ARG REDIS_PECL_VERSION=6.1.0 +# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?.*)$ +ARG COMPOSER_VERSION=2.8.6 + +RUN apk add -U --no-cache autoconf \ + aspell-dev \ + aspell-libs \ + bash \ + c-client \ + cyrus-sasl-dev \ + freetype \ + freetype-dev \ + g++ \ + git \ + gettext \ + gettext-dev \ + gmp-dev \ + gnupg \ + icu-dev \ + icu-libs \ + imagemagick \ + imagemagick-dev \ + imap-dev \ + jq \ + libavif \ + libavif-dev \ + libjpeg-turbo \ + libjpeg-turbo-dev \ + libmemcached \ + libmemcached-dev \ + libpng \ + libpng-dev \ + libressl \ + libressl-dev \ + librsvg \ + libtool \ + libwebp-dev \ + libxml2-dev \ + libxpm \ + libxpm-dev \ + libzip \ + libzip-dev \ + linux-headers \ + make \ + mysql-client \ + openldap-dev \ + pcre-dev \ + re2c \ + redis \ + samba-client \ + zlib-dev \ + tzdata \ + && pecl install APCu-${APCU_PECL_VERSION} \ + && pecl install imagick-${IMAGICK_PECL_VERSION} \ + && pecl install mailparse-${MAILPARSE_PECL_VERSION} \ + && pecl install memcached-${MEMCACHED_PECL_VERSION} \ + && pecl install redis-${REDIS_PECL_VERSION} \ + && docker-php-ext-enable apcu imagick memcached mailparse redis \ + && pecl clear-cache \ + && docker-php-ext-configure intl \ + && docker-php-ext-configure exif \ + && docker-php-ext-configure gd --with-freetype=/usr/include/ \ + --with-jpeg=/usr/include/ \ + --with-webp \ + --with-xpm \ + --with-avif \ + && docker-php-ext-install -j 4 exif gd gettext intl ldap opcache pcntl pdo pdo_mysql pspell soap sockets zip bcmath gmp \ + && docker-php-ext-configure imap --with-imap --with-imap-ssl \ + && docker-php-ext-install -j 4 imap \ + && curl --silent --show-error https://getcomposer.org/installer | php -- --version=${COMPOSER_VERSION} \ + && mv composer.phar /usr/local/bin/composer \ + && chmod +x /usr/local/bin/composer \ + && apk del --purge autoconf \ + aspell-dev \ + cyrus-sasl-dev \ + freetype-dev \ + g++ \ + gettext-dev \ + icu-dev \ + imagemagick-dev \ + imap-dev \ + libavif-dev \ + libjpeg-turbo-dev \ + libmemcached-dev \ + libpng-dev \ + libressl-dev \ + libwebp-dev \ + libxml2-dev \ + libxpm-dev \ + libzip-dev \ + linux-headers \ + make \ + openldap-dev \ + pcre-dev \ + zlib-dev + +COPY ./docker-entrypoint.sh / + +ENTRYPOINT ["/docker-entrypoint.sh"] + +CMD ["php-fpm"] diff --git a/mailcow/data/Dockerfiles/phpfpm/docker-entrypoint.sh b/mailcow/data/Dockerfiles/phpfpm/docker-entrypoint.sh new file mode 100755 index 0000000..0d09ac5 --- /dev/null +++ b/mailcow/data/Dockerfiles/phpfpm/docker-entrypoint.sh @@ -0,0 +1,225 @@ +#!/bin/bash + +function array_by_comma { local IFS=","; echo "$*"; } + +# Wait for containers +while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do + echo "Waiting for SQL..." + sleep 2 +done + +# Do not attempt to write to slave +if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then + REDIS_HOST=$REDIS_SLAVEOF_IP + REDIS_PORT=$REDIS_SLAVEOF_PORT +else + REDIS_HOST="redis" + REDIS_PORT="6379" +fi +REDIS_CMDLINE="redis-cli -h ${REDIS_HOST} -p ${REDIS_PORT} -a ${REDISPASS} --no-auth-warning" + +until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do + echo "Waiting for Redis..." + sleep 2 +done + +# Set redis session store +echo -n ' +session.save_handler = redis +session.save_path = "tcp://'${REDIS_HOST}':'${REDIS_PORT}'?auth='${REDISPASS}'" +' > /usr/local/etc/php/conf.d/session_store.ini + +# Check mysql_upgrade (master and slave) +CONTAINER_ID= +until [[ ! -z "${CONTAINER_ID}" ]] && [[ "${CONTAINER_ID}" =~ ^[[:alnum:]]*$ ]]; do + CONTAINER_ID=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"mysql-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null) + echo "Could not get mysql-mailcow container id... trying again" + sleep 2 +done +echo "MySQL @ ${CONTAINER_ID}" +SQL_LOOP_C=0 +SQL_CHANGED=0 +until [[ ${SQL_UPGRADE_STATUS} == 'success' ]]; do + if [ ${SQL_LOOP_C} -gt 4 ]; then + echo "Tried to upgrade MySQL and failed, giving up after ${SQL_LOOP_C} retries and starting container (oops, not good)" + break + fi + SQL_FULL_UPGRADE_RETURN=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_upgrade"}' --silent -H 'Content-type: application/json') + SQL_UPGRADE_STATUS=$(echo ${SQL_FULL_UPGRADE_RETURN} | jq -r .type) + SQL_LOOP_C=$((SQL_LOOP_C+1)) + echo "SQL upgrade iteration #${SQL_LOOP_C}" + if [[ ${SQL_UPGRADE_STATUS} == 'warning' ]]; then + SQL_CHANGED=1 + echo "MySQL applied an upgrade, debug output:" + echo ${SQL_FULL_UPGRADE_RETURN} + sleep 3 + while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do + echo "Waiting for SQL to return, please wait" + sleep 2 + done + continue + elif [[ ${SQL_UPGRADE_STATUS} == 'success' ]]; then + echo "MySQL is up-to-date - debug output:" + echo ${SQL_FULL_UPGRADE_RETURN} + else + echo "No valid reponse for mysql_upgrade was received, debug output:" + echo ${SQL_FULL_UPGRADE_RETURN} + fi +done + +# doing post-installation stuff, if SQL was upgraded (master and slave) +if [ ${SQL_CHANGED} -eq 1 ]; then + POSTFIX=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null) + if [[ -z "${POSTFIX}" ]] || ! [[ "${POSTFIX}" =~ ^[[:alnum:]]*$ ]]; then + echo "Could not determine Postfix container ID, skipping Postfix restart." + else + echo "Restarting Postfix" + curl -X POST --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${POSTFIX}/restart | jq -r '.msg' + echo "Sleeping 5 seconds..." + sleep 5 + fi +fi + +# Check mysql tz import (master and slave) +TZ_CHECK=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT CONVERT_TZ('2019-11-02 23:33:00','Europe/Berlin','UTC') AS time;" -BN 2> /dev/null) +if [[ -z ${TZ_CHECK} ]] || [[ "${TZ_CHECK}" == "NULL" ]]; then + SQL_FULL_TZINFO_IMPORT_RETURN=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_tzinfo_to_sql"}' --silent -H 'Content-type: application/json') + echo "MySQL mysql_tzinfo_to_sql - debug output:" + echo ${SQL_FULL_TZINFO_IMPORT_RETURN} +fi + +if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + echo "We are master, preparing..." + # Set a default release format + if [[ -z $(${REDIS_CMDLINE} --raw GET Q_RELEASE_FORMAT) ]]; then + ${REDIS_CMDLINE} --raw SET Q_RELEASE_FORMAT raw + fi + + # Set max age of q items - if unset + if [[ -z $(${REDIS_CMDLINE} --raw GET Q_MAX_AGE) ]]; then + ${REDIS_CMDLINE} --raw SET Q_MAX_AGE 365 + fi + + # Set default password policy - if unset + if [[ -z $(${REDIS_CMDLINE} --raw HGET PASSWD_POLICY length) ]]; then + ${REDIS_CMDLINE} --raw HSET PASSWD_POLICY length 6 + ${REDIS_CMDLINE} --raw HSET PASSWD_POLICY chars 0 + ${REDIS_CMDLINE} --raw HSET PASSWD_POLICY special_chars 0 + ${REDIS_CMDLINE} --raw HSET PASSWD_POLICY lowerupper 0 + ${REDIS_CMDLINE} --raw HSET PASSWD_POLICY numbers 0 + fi + + # Trigger db init + echo "Running DB init..." + php -c /usr/local/etc/php -f /web/inc/init_db.inc.php + + # Recreating domain map + echo "Rebuilding domain map in Redis..." + declare -a DOMAIN_ARR + ${REDIS_CMDLINE} DEL DOMAIN_MAP > /dev/null + while read line + do + DOMAIN_ARR+=("$line") + done < <(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain" -Bs) + while read line + do + DOMAIN_ARR+=("$line") + done < <(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT alias_domain FROM alias_domain" -Bs) + + if [[ ! -z ${DOMAIN_ARR} ]]; then + for domain in "${DOMAIN_ARR[@]}"; do + ${REDIS_CMDLINE} HSET DOMAIN_MAP ${domain} 1 > /dev/null + done + fi + + # Set API options if env vars are not empty + if [[ ${API_ALLOW_FROM} != "invalid" ]] && [[ ! -z ${API_ALLOW_FROM} ]]; then + IFS=',' read -r -a API_ALLOW_FROM_ARR <<< "${API_ALLOW_FROM}" + declare -a VALIDATED_API_ALLOW_FROM_ARR + REGEX_IP6='^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}(/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$' + REGEX_IP4='^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(/([0-9]|[1-2][0-9]|3[0-2]))?$' + for IP in "${API_ALLOW_FROM_ARR[@]}"; do + if [[ ${IP} =~ ${REGEX_IP6} ]] || [[ ${IP} =~ ${REGEX_IP4} ]]; then + VALIDATED_API_ALLOW_FROM_ARR+=("${IP}") + fi + done + VALIDATED_IPS=$(array_by_comma ${VALIDATED_API_ALLOW_FROM_ARR[*]}) + if [[ ! -z ${VALIDATED_IPS} ]]; then + if [[ ${API_KEY} != "invalid" ]] && [[ ! -z ${API_KEY} ]]; then + mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF +DELETE FROM api WHERE access = 'rw'; +INSERT INTO api (api_key, active, allow_from, access) VALUES ("${API_KEY}", "1", "${VALIDATED_IPS}", "rw"); +EOF + fi + if [[ ${API_KEY_READ_ONLY} != "invalid" ]] && [[ ! -z ${API_KEY_READ_ONLY} ]]; then + mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF +DELETE FROM api WHERE access = 'ro'; +INSERT INTO api (api_key, active, allow_from, access) VALUES ("${API_KEY_READ_ONLY}", "1", "${VALIDATED_IPS}", "ro"); +EOF + fi + fi + fi + + # Create events (master only, STATUS for event on slave will be SLAVESIDE_DISABLED) + mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF +DROP EVENT IF EXISTS clean_spamalias; +DELIMITER // +CREATE EVENT clean_spamalias +ON SCHEDULE EVERY 1 DAY DO +BEGIN + DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP(); +END; +// +DELIMITER ; +DROP EVENT IF EXISTS clean_oauth2; +DELIMITER // +CREATE EVENT clean_oauth2 +ON SCHEDULE EVERY 1 DAY DO +BEGIN + DELETE FROM oauth_refresh_tokens WHERE expires < NOW(); + DELETE FROM oauth_access_tokens WHERE expires < NOW(); + DELETE FROM oauth_authorization_codes WHERE expires < NOW(); +END; +// +DELIMITER ; +DROP EVENT IF EXISTS clean_sasl_log; +DELIMITER // +CREATE EVENT clean_sasl_log +ON SCHEDULE EVERY 1 DAY DO +BEGIN + DELETE sasl_log.* FROM sasl_log + LEFT JOIN ( + SELECT username, service, MAX(datetime) AS lastdate + FROM sasl_log + GROUP BY username, service + ) AS last ON sasl_log.username = last.username AND sasl_log.service = last.service + WHERE datetime < DATE_SUB(NOW(), INTERVAL 31 DAY) AND datetime < lastdate; + DELETE FROM sasl_log + WHERE username NOT IN (SELECT username FROM mailbox) AND + datetime < DATE_SUB(NOW(), INTERVAL 31 DAY); +END; +// +DELIMITER ; +EOF +fi + +# Create dummy for custom overrides of mailcow style +[[ ! -f /web/css/build/0081-custom-mailcow.css ]] && echo '/* Autogenerated by mailcow */' > /web/css/build/0081-custom-mailcow.css + +# Fix permissions for global filters +chown -R 82:82 /global_sieve/* + +# Fix permissions on twig cache folder +chown -R 82:82 /web/templates/cache +# Clear cache +find /web/templates/cache/* -not -name '.gitkeep' -delete + +# Run hooks +for file in /hooks/*; do + if [ -x "${file}" ]; then + echo "Running hook ${file}" + "${file}" + fi +done + +exec "$@" diff --git a/mailcow/data/Dockerfiles/postfix/Dockerfile b/mailcow/data/Dockerfiles/postfix/Dockerfile new file mode 100644 index 0000000..5449360 --- /dev/null +++ b/mailcow/data/Dockerfiles/postfix/Dockerfile @@ -0,0 +1,63 @@ +FROM debian:bookworm-slim + +LABEL maintainer = "The Infrastructure Company GmbH " + +ARG DEBIAN_FRONTEND=noninteractive +ENV LC_ALL C + +RUN dpkg-divert --local --rename --add /sbin/initctl \ + && ln -sf /bin/true /sbin/initctl \ + && dpkg-divert --local --rename --add /usr/bin/ischroot \ + && ln -sf /bin/true /usr/bin/ischroot + +# Add groups and users before installing Postfix to not break compatibility +RUN groupadd -g 102 postfix \ + && groupadd -g 103 postdrop \ + && useradd -g postfix -u 101 -d /var/spool/postfix -s /usr/sbin/nologin postfix \ + && apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + dirmngr \ + dnsutils \ + gnupg \ + libsasl2-modules \ + mariadb-client \ + perl \ + postfix \ + postfix-mysql \ + postfix-pcre \ + redis-tools \ + sasl2-bin \ + sudo \ + supervisor \ + syslog-ng \ + syslog-ng-core \ + syslog-ng-mod-redis \ + tzdata \ + && rm -rf /var/lib/apt/lists/* \ + && touch /etc/default/locale \ + && printf '#!/bin/bash\n/usr/sbin/postconf -c /opt/postfix/conf "$@"' > /usr/local/sbin/postconf \ + && chmod +x /usr/local/sbin/postconf + +COPY supervisord.conf /etc/supervisor/supervisord.conf +COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf +COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf +COPY postfix.sh /opt/postfix.sh +COPY rspamd-pipe-ham /usr/local/bin/rspamd-pipe-ham +COPY rspamd-pipe-spam /usr/local/bin/rspamd-pipe-spam +COPY whitelist_forwardinghosts.sh /usr/local/bin/whitelist_forwardinghosts.sh +COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh +COPY docker-entrypoint.sh /docker-entrypoint.sh + +RUN chmod +x /opt/postfix.sh \ + /usr/local/bin/rspamd-pipe-ham \ + /usr/local/bin/rspamd-pipe-spam \ + /usr/local/bin/whitelist_forwardinghosts.sh \ + /usr/local/sbin/stop-supervisor.sh +RUN rm -rf /tmp/* /var/tmp/* + +EXPOSE 588 + +ENTRYPOINT ["/docker-entrypoint.sh"] + +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] diff --git a/mailcow/data/Dockerfiles/postfix/docker-entrypoint.sh b/mailcow/data/Dockerfiles/postfix/docker-entrypoint.sh new file mode 100755 index 0000000..7b6c5d4 --- /dev/null +++ b/mailcow/data/Dockerfiles/postfix/docker-entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Run hooks +for file in /hooks/*; do + if [ -x "${file}" ]; then + echo "Running hook ${file}" + "${file}" + fi +done + +if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then + cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf +fi + +# Fix OpenSSL 3.X TLS1.0, 1.1 support (https://community.mailcow.email/d/4062-hi-all/20) +if grep -qE '\!SSLv2|\!SSLv3|>=TLSv1(\.[0-1])?$' /opt/postfix/conf/main.cf /opt/postfix/conf/extra.cf; then + sed -i '/\[openssl_init\]/a ssl_conf = ssl_configuration' /etc/ssl/openssl.cnf + + echo "[ssl_configuration]" >> /etc/ssl/openssl.cnf + echo "system_default = tls_system_default" >> /etc/ssl/openssl.cnf + echo "[tls_system_default]" >> /etc/ssl/openssl.cnf + echo "MinProtocol = TLSv1" >> /etc/ssl/openssl.cnf + echo "CipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf +fi + +exec "$@" diff --git a/mailcow/data/Dockerfiles/postfix/postfix.sh b/mailcow/data/Dockerfiles/postfix/postfix.sh new file mode 100755 index 0000000..e5dbf88 --- /dev/null +++ b/mailcow/data/Dockerfiles/postfix/postfix.sh @@ -0,0 +1,527 @@ +#!/bin/bash + +trap "postfix stop" EXIT + +[[ ! -d /opt/postfix/conf/sql/ ]] && mkdir -p /opt/postfix/conf/sql/ + +# Wait for MySQL to warm-up +while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do + echo "Waiting for database to come up..." + sleep 2 +done + +until dig +short mailcow.email > /dev/null; do + echo "Waiting for DNS..." + sleep 1 +done + +cat < /etc/aliases +# Autogenerated by mailcow +null: /dev/null +watchdog: /dev/null +ham: "|/usr/local/bin/rspamd-pipe-ham" +spam: "|/usr/local/bin/rspamd-pipe-spam" +EOF +newaliases; + +# create sni configuration +if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + echo -n "" > /opt/postfix/conf/sni.map +else + echo -n "" > /opt/postfix/conf/sni.map; + for cert_dir in /etc/ssl/mail/*/ ; do + if [[ ! -f ${cert_dir}domains ]] || [[ ! -f ${cert_dir}cert.pem ]] || [[ ! -f ${cert_dir}key.pem ]]; then + continue; + fi + IFS=" " read -r -a domains <<< "$(cat "${cert_dir}domains")" + for domain in "${domains[@]}"; do + echo -n "${domain} ${cert_dir}key.pem ${cert_dir}cert.pem" >> /opt/postfix/conf/sni.map; + echo "" >> /opt/postfix/conf/sni.map; + done + done +fi +postmap -F hash:/opt/postfix/conf/sni.map; + +cat < /opt/postfix/conf/sql/mysql_relay_ne.cf +# Autogenerated by mailcow +user = ${DBUSER} +password = ${DBPASS} +hosts = unix:/var/run/mysqld/mysqld.sock +dbname = ${DBNAME} +query = SELECT IF(EXISTS(SELECT address, domain FROM alias + WHERE address = '%s' + AND domain IN ( + SELECT domain FROM domain + WHERE backupmx = '1' + AND relay_all_recipients = '1' + AND relay_unknown_only = '1') + + ), 'lmtp:inet:dovecot:24', NULL) AS 'transport' +EOF + +cat < /opt/postfix/conf/sql/mysql_relay_recipient_maps.cf +# Autogenerated by mailcow +user = ${DBUSER} +password = ${DBPASS} +hosts = unix:/var/run/mysqld/mysqld.sock +dbname = ${DBNAME} +query = SELECT DISTINCT + CASE WHEN '%d' IN ( + SELECT domain FROM domain + WHERE relay_all_recipients=1 + AND domain='%d' + AND backupmx=1 + ) + THEN '%s' ELSE ( + SELECT goto FROM alias WHERE address='%s' AND active='1' + ) + END AS result; +EOF + +cat < /opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf +# Autogenerated by mailcow +user = ${DBUSER} +password = ${DBPASS} +hosts = unix:/var/run/mysqld/mysqld.sock +dbname = ${DBNAME} +query = SELECT CONCAT(policy, ' ', parameters) AS tls_policy FROM tls_policy_override WHERE active = '1' AND dest = '%s' +EOF + +cat < /opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf +# Autogenerated by mailcow +user = ${DBUSER} +password = ${DBPASS} +hosts = unix:/var/run/mysqld/mysqld.sock +dbname = ${DBNAME} +query = SELECT IF(EXISTS( + SELECT 'TLS_ACTIVE' FROM alias + LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto + WHERE (address='%s' + OR address IN ( + SELECT CONCAT('%u', '@', target_domain) FROM alias_domain + WHERE alias_domain='%d' + ) + ) AND JSON_UNQUOTE(JSON_VALUE(attributes, '$.tls_enforce_in')) = '1' AND mailbox.active = '1' + ), 'reject_plaintext_session', NULL) AS 'tls_enforce_in'; +EOF + +cat < /opt/postfix/conf/sql/mysql_sender_dependent_default_transport_maps.cf +# Autogenerated by mailcow +user = ${DBUSER} +password = ${DBPASS} +hosts = unix:/var/run/mysqld/mysqld.sock +dbname = ${DBNAME} +query = SELECT GROUP_CONCAT(transport SEPARATOR '') AS transport_maps + FROM ( + SELECT IF(EXISTS(SELECT 'smtp_type' FROM alias + LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto + WHERE (address = '%s' + OR address IN ( + SELECT CONCAT('%u', '@', target_domain) FROM alias_domain + WHERE alias_domain = '%d' + ) + ) + AND JSON_UNQUOTE(JSON_VALUE(attributes, '$.tls_enforce_out')) = '1' + AND mailbox.active = '1' + ), 'smtp_enforced_tls:', 'smtp:') AS 'transport' + UNION ALL + SELECT COALESCE( + (SELECT hostname FROM relayhosts + LEFT OUTER JOIN mailbox ON JSON_UNQUOTE(JSON_VALUE(mailbox.attributes, '$.relayhost')) = relayhosts.id + WHERE relayhosts.active = '1' + AND ( + mailbox.username IN (SELECT alias.goto from alias + JOIN mailbox ON mailbox.username = alias.goto + WHERE alias.active = '1' + AND alias.address = '%s' + AND alias.address NOT LIKE '@%%' + ) + ) + ), + (SELECT hostname FROM relayhosts + LEFT OUTER JOIN domain ON domain.relayhost = relayhosts.id + WHERE relayhosts.active = '1' + AND (domain.domain = '%d' + OR domain.domain IN ( + SELECT target_domain FROM alias_domain + WHERE alias_domain = '%d' + ) + ) + ) + ) + ) AS transport_view; +EOF + +cat < /opt/postfix/conf/sql/mysql_transport_maps.cf +# Autogenerated by mailcow +user = ${DBUSER} +password = ${DBPASS} +hosts = unix:/var/run/mysqld/mysqld.sock +dbname = ${DBNAME} +query = SELECT CONCAT('smtp_via_transport_maps:', nexthop) AS transport FROM transports + WHERE active = '1' + AND destination = '%s'; +EOF + +cat < /opt/postfix/conf/sql/mysql_virtual_resource_maps.cf +# Autogenerated by mailcow +user = ${DBUSER} +password = ${DBPASS} +hosts = unix:/var/run/mysqld/mysqld.sock +dbname = ${DBNAME} +query = SELECT 'null@localhost' FROM mailbox + WHERE kind REGEXP 'location|thing|group' AND username = '%s'; +EOF + +cat < /opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf +# Autogenerated by mailcow +user = ${DBUSER} +password = ${DBPASS} +hosts = unix:/var/run/mysqld/mysqld.sock +dbname = ${DBNAME} +query = SELECT CONCAT_WS(':', username, password) AS auth_data FROM relayhosts + WHERE id IN ( + SELECT COALESCE( + (SELECT id FROM relayhosts + LEFT OUTER JOIN domain ON domain.relayhost = relayhosts.id + WHERE relayhosts.active = '1' + AND (domain.domain = '%d' + OR domain.domain IN ( + SELECT target_domain FROM alias_domain + WHERE alias_domain = '%d' + ) + ) + ), + (SELECT id FROM relayhosts + LEFT OUTER JOIN mailbox ON JSON_UNQUOTE(JSON_VALUE(mailbox.attributes, '$.relayhost')) = relayhosts.id + WHERE relayhosts.active = '1' + AND ( + mailbox.username IN ( + SELECT alias.goto from alias + JOIN mailbox ON mailbox.username = alias.goto + WHERE alias.active = '1' + AND alias.address = '%s' + AND alias.address NOT LIKE '@%%' + ) + ) + ) + ) + ) + AND active = '1' + AND username != ''; +EOF + +cat < /opt/postfix/conf/sql/mysql_sasl_passwd_maps_transport_maps.cf +# Autogenerated by mailcow +user = ${DBUSER} +password = ${DBPASS} +hosts = unix:/var/run/mysqld/mysqld.sock +dbname = ${DBNAME} +query = SELECT CONCAT_WS(':', username, password) AS auth_data FROM transports + WHERE nexthop = '%s' + AND active = '1' + AND username != '' + LIMIT 1; +EOF + +cat < /opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf +# Autogenerated by mailcow +user = ${DBUSER} +password = ${DBPASS} +hosts = unix:/var/run/mysqld/mysqld.sock +dbname = ${DBNAME} +query = SELECT username FROM mailbox, alias_domain + WHERE alias_domain.alias_domain = '%d' + AND mailbox.username = CONCAT('%u', '@', alias_domain.target_domain) + AND (mailbox.active = '1' OR mailbox.active = '2') + AND alias_domain.active='1' +EOF + +cat < /opt/postfix/conf/sql/mysql_virtual_alias_maps.cf +# Autogenerated by mailcow +user = ${DBUSER} +password = ${DBPASS} +hosts = unix:/var/run/mysqld/mysqld.sock +dbname = ${DBNAME} +query = SELECT goto FROM alias + WHERE address='%s' + AND (active='1' OR active='2'); +EOF + +cat < /opt/postfix/conf/sql/mysql_recipient_bcc_maps.cf +# Autogenerated by mailcow +user = ${DBUSER} +password = ${DBPASS} +hosts = unix:/var/run/mysqld/mysqld.sock +dbname = ${DBNAME} +query = SELECT bcc_dest FROM bcc_maps + WHERE local_dest='%s' + AND type='rcpt' + AND active='1'; +EOF + +cat < /opt/postfix/conf/sql/mysql_sender_bcc_maps.cf +# Autogenerated by mailcow +user = ${DBUSER} +password = ${DBPASS} +hosts = unix:/var/run/mysqld/mysqld.sock +dbname = ${DBNAME} +query = SELECT bcc_dest FROM bcc_maps + WHERE local_dest='%s' + AND type='sender' + AND active='1'; +EOF + +cat < /opt/postfix/conf/sql/mysql_recipient_canonical_maps.cf +# Autogenerated by mailcow +user = ${DBUSER} +password = ${DBPASS} +hosts = unix:/var/run/mysqld/mysqld.sock +dbname = ${DBNAME} +query = SELECT new_dest FROM recipient_maps + WHERE old_dest='%s' + AND active='1'; +EOF + +cat < /opt/postfix/conf/sql/mysql_virtual_domains_maps.cf +# Autogenerated by mailcow +user = ${DBUSER} +password = ${DBPASS} +hosts = unix:/var/run/mysqld/mysqld.sock +dbname = ${DBNAME} +query = SELECT alias_domain from alias_domain WHERE alias_domain='%s' AND active='1' + UNION + SELECT domain FROM domain + WHERE domain='%s' + AND active = '1' + AND backupmx = '0' +EOF + +cat < /opt/postfix/conf/sql/mysql_virtual_mailbox_maps.cf +# Autogenerated by mailcow +user = ${DBUSER} +password = ${DBPASS} +hosts = unix:/var/run/mysqld/mysqld.sock +dbname = ${DBNAME} +query = SELECT CONCAT(JSON_UNQUOTE(JSON_VALUE(attributes, '$.mailbox_format')), mailbox_path_prefix, '%d/%u/') FROM mailbox WHERE username='%s' AND (active = '1' OR active = '2') +EOF + +cat < /opt/postfix/conf/sql/mysql_virtual_relay_domain_maps.cf +# Autogenerated by mailcow +user = ${DBUSER} +password = ${DBPASS} +hosts = unix:/var/run/mysqld/mysqld.sock +dbname = ${DBNAME} +query = SELECT domain FROM domain WHERE domain='%s' AND backupmx = '1' AND active = '1' +EOF + +cat < /opt/postfix/conf/sql/mysql_virtual_sender_acl.cf +# Autogenerated by mailcow +user = ${DBUSER} +password = ${DBPASS} +hosts = unix:/var/run/mysqld/mysqld.sock +dbname = ${DBNAME} +# First select queries domain and alias_domain to determine if domains are active. +query = SELECT goto FROM alias + WHERE id IN ( + SELECT COALESCE ( + ( + SELECT id FROM alias + WHERE address='%s' + AND (active='1' OR active='2') + ), ( + SELECT id FROM alias + WHERE address='@%d' + AND (active='1' OR active='2') + ) + ) + ) + AND active='1' + AND (domain IN + (SELECT domain FROM domain + WHERE domain='%d' + AND active='1') + OR domain in ( + SELECT alias_domain FROM alias_domain + WHERE alias_domain='%d' + AND active='1' + ) + ) + UNION + SELECT logged_in_as FROM sender_acl + WHERE send_as='@%d' + OR send_as='%s' + OR send_as='*' + OR send_as IN ( + SELECT CONCAT('@',target_domain) FROM alias_domain + WHERE alias_domain = '%d') + OR send_as IN ( + SELECT CONCAT('%u','@',target_domain) FROM alias_domain + WHERE alias_domain = '%d') + AND logged_in_as NOT IN ( + SELECT goto FROM alias + WHERE address='%s') + UNION + SELECT username FROM mailbox, alias_domain + WHERE alias_domain.alias_domain = '%d' + AND mailbox.username = CONCAT('%u','@',alias_domain.target_domain) + AND (mailbox.active = '1' OR mailbox.active ='2') + AND alias_domain.active='1'; +EOF + +# MX based routing +cat < /opt/postfix/conf/sql/mysql_mbr_access_maps.cf +# Autogenerated by mailcow +user = ${DBUSER} +password = ${DBPASS} +hosts = unix:/var/run/mysqld/mysqld.sock +dbname = ${DBNAME} +query = SELECT CONCAT('FILTER smtp_via_transport_maps:', nexthop) as transport FROM transports + WHERE '%s' REGEXP destination + AND active='1' + AND is_mx_based='1'; +EOF + +cat < /opt/postfix/conf/sql/mysql_virtual_spamalias_maps.cf +# Autogenerated by mailcow +user = ${DBUSER} +password = ${DBPASS} +hosts = unix:/var/run/mysqld/mysqld.sock +dbname = ${DBNAME} +query = SELECT goto FROM spamalias + WHERE address='%s' + AND validity >= UNIX_TIMESTAMP() +EOF + +if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then + cat < /opt/postfix/conf/dns_blocklists.cf +# This file can be edited. +# Delete this file and restart postfix container to revert any changes. +postscreen_dnsbl_sites = wl.mailspike.net=127.0.0.[18;19;20]*-2 + hostkarma.junkemailfilter.com=127.0.0.1*-2 + list.dnswl.org=127.0.[0..255].0*-2 + list.dnswl.org=127.0.[0..255].1*-4 + list.dnswl.org=127.0.[0..255].2*-6 + list.dnswl.org=127.0.[0..255].3*-8 + bl.spamcop.net*2 + bl.suomispam.net*2 + hostkarma.junkemailfilter.com=127.0.0.2*3 + hostkarma.junkemailfilter.com=127.0.0.4*2 + hostkarma.junkemailfilter.com=127.0.1.2*1 + backscatter.spameatingmonkey.net*2 + bl.ipv6.spameatingmonkey.net*2 + bl.spameatingmonkey.net*2 + b.barracudacentral.org=127.0.0.2*7 + bl.mailspike.net=127.0.0.2*5 + bl.mailspike.net=127.0.0.[10;11;12]*4 +EOF +fi + +# Remove discontinued DNSBLs from existing dns_blocklists.cf +sed -i '/ix\.dnsbl\.manitu\.net\*2/d' /opt/postfix/conf/dns_blocklists.cf # Nixspam + +DNSBL_CONFIG=$(grep -v '^#' /opt/postfix/conf/dns_blocklists.cf | grep '\S') + +if [ ! -z "$DNSBL_CONFIG" ]; then + echo -e "\e[33mChecking if ASN for your IP is listed for Spamhaus Bad ASN List...\e[0m" + if [ -n "$SPAMHAUS_DQS_KEY" ]; then + echo -e "\e[32mDetected SPAMHAUS_DQS_KEY variable from mailcow.conf...\e[0m" + echo -e "\e[33mUsing DQS Blocklists from Spamhaus!\e[0m" + SPAMHAUS_DNSBL_CONFIG=$(cat < /opt/postfix/conf/dnsbl_reply.map +# Autogenerated by mailcow, using Spamhaus DQS reply domains +${SPAMHAUS_DQS_KEY}.sbl.dq.spamhaus.net sbl.spamhaus.org +${SPAMHAUS_DQS_KEY}.xbl.dq.spamhaus.net xbl.spamhaus.org +${SPAMHAUS_DQS_KEY}.pbl.dq.spamhaus.net pbl.spamhaus.org +${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net zen.spamhaus.org +${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net dbl.spamhaus.org +${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net zrd.spamhaus.org +EOF + ) + else + if [ -f "/opt/postfix/conf/dnsbl_reply.map" ]; then + rm /opt/postfix/conf/dnsbl_reply.map + fi + response=$(curl --connect-timeout 15 --max-time 30 -s -o /dev/null -w "%{http_code}" "https://asn-check.mailcow.email") + if [ "$response" -eq 503 ]; then + echo -e "\e[31mThe AS of your IP is listed as a banned AS from Spamhaus!\e[0m" + echo -e "\e[33mNo SPAMHAUS_DQS_KEY found... Skipping Spamhaus blocklists entirely!\e[0m" + SPAMHAUS_DNSBL_CONFIG="" + elif [ "$response" -eq 200 ]; then + echo -e "\e[32mThe AS of your IP is NOT listed as a banned AS from Spamhaus!\e[0m" + echo -e "\e[33mUsing the open Spamhaus blocklists.\e[0m" + SPAMHAUS_DNSBL_CONFIG=$(cat <> /opt/postfix/conf/main.cf +# Append postscreen dnsbl sites to main.cf +if [ ! -z "$DNSBL_CONFIG" ]; then + echo -e "${DNSBL_CONFIG}\n${SPAMHAUS_DNSBL_CONFIG}" >> /opt/postfix/conf/main.cf +fi +# Append user overrides +echo -e "\n# User Overrides" >> /opt/postfix/conf/main.cf +touch /opt/postfix/conf/extra.cf +sed -i '/\$myhostname/! { /myhostname/d }' /opt/postfix/conf/extra.cf +echo -e "myhostname = ${MAILCOW_HOSTNAME}\n$(cat /opt/postfix/conf/extra.cf)" > /opt/postfix/conf/extra.cf +cat /opt/postfix/conf/extra.cf >> /opt/postfix/conf/main.cf + +if [ ! -f /opt/postfix/conf/custom_transport.pcre ]; then + echo "Creating dummy custom_transport.pcre" + touch /opt/postfix/conf/custom_transport.pcre +fi + +if [[ ! -f /opt/postfix/conf/custom_postscreen_whitelist.cidr ]]; then + echo "Creating dummy custom_postscreen_whitelist.cidr" + cat < /opt/postfix/conf/custom_postscreen_whitelist.cidr +# Autogenerated by mailcow +# Rules are evaluated in the order as specified. +# Blacklist 192.168.* except 192.168.0.1. +# 192.168.0.1 permit +# 192.168.0.0/16 reject +EOF +fi + +# Fix Postfix permissions +chown -R root:postfix /opt/postfix/conf/sql/ /opt/postfix/conf/custom_transport.pcre +chmod 640 /opt/postfix/conf/sql/*.cf /opt/postfix/conf/custom_transport.pcre +chgrp -R postdrop /var/spool/postfix/public +chgrp -R postdrop /var/spool/postfix/maildrop +postfix set-permissions + +# Checking if there is a leftover of a crashed postfix container before starting a new one +if [ -e /var/spool/postfix/pid/master.pid ]; then + rm -rf /var/spool/postfix/pid/master.pid +fi + +# Check Postfix configuration +postconf -c /opt/postfix/conf > /dev/null + +if [[ $? != 0 ]]; then + echo "Postfix configuration error, refusing to start." + exit 1 +else + postfix -c /opt/postfix/conf start + sleep 126144000 +fi diff --git a/mailcow/data/Dockerfiles/postfix/rspamd-pipe-ham b/mailcow/data/Dockerfiles/postfix/rspamd-pipe-ham new file mode 100755 index 0000000..9b26817 --- /dev/null +++ b/mailcow/data/Dockerfiles/postfix/rspamd-pipe-ham @@ -0,0 +1,9 @@ +#!/bin/bash +FILE=/tmp/mail$$ +cat > $FILE +trap "/bin/rm -f $FILE" 0 1 2 3 13 15 + +cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/learnham +cat ${FILE} | /usr/bin/curl -H "Flag: 13" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/fuzzyadd + +exit 0 diff --git a/mailcow/data/Dockerfiles/postfix/rspamd-pipe-spam b/mailcow/data/Dockerfiles/postfix/rspamd-pipe-spam new file mode 100755 index 0000000..d06aa91 --- /dev/null +++ b/mailcow/data/Dockerfiles/postfix/rspamd-pipe-spam @@ -0,0 +1,9 @@ +#!/bin/bash +FILE=/tmp/mail$$ +cat > $FILE +trap "/bin/rm -f $FILE" 0 1 2 3 13 15 + +cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/learnspam +cat ${FILE} | /usr/bin/curl -H "Flag: 11" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/fuzzyadd + +exit 0 diff --git a/mailcow/data/Dockerfiles/postfix/stop-supervisor.sh b/mailcow/data/Dockerfiles/postfix/stop-supervisor.sh new file mode 100755 index 0000000..5394490 --- /dev/null +++ b/mailcow/data/Dockerfiles/postfix/stop-supervisor.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +printf "READY\n"; + +while read line; do + echo "Processing Event: $line" >&2; + kill -3 $(cat "/var/run/supervisord.pid") +done < /dev/stdin diff --git a/mailcow/data/Dockerfiles/postfix/supervisord.conf b/mailcow/data/Dockerfiles/postfix/supervisord.conf new file mode 100644 index 0000000..ba70f8e --- /dev/null +++ b/mailcow/data/Dockerfiles/postfix/supervisord.conf @@ -0,0 +1,25 @@ +[supervisord] +pidfile=/var/run/supervisord.pid +nodaemon=true +user=root + +[program:syslog-ng] +command=/usr/sbin/syslog-ng --foreground --no-caps +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autostart=true + +[program:postfix] +command=/opt/postfix.sh +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autorestart=true +startsecs=10 + +[eventlistener:processes] +command=/usr/local/sbin/stop-supervisor.sh +events=PROCESS_STATE_STOPPED, PROCESS_STATE_EXITED, PROCESS_STATE_FATAL diff --git a/mailcow/data/Dockerfiles/postfix/syslog-ng-redis_slave.conf b/mailcow/data/Dockerfiles/postfix/syslog-ng-redis_slave.conf new file mode 100644 index 0000000..8e15932 --- /dev/null +++ b/mailcow/data/Dockerfiles/postfix/syslog-ng-redis_slave.conf @@ -0,0 +1,55 @@ +@version: 3.38 +@include "scl.conf" +options { + chain_hostnames(off); + flush_lines(0); + use_dns(no); + dns_cache(no); + use_fqdn(no); + owner("root"); group("adm"); perm(0640); + stats_freq(0); + bad_hostname("^gconfd$"); +}; +source s_src { + unix-stream("/dev/log"); + internal(); +}; +destination d_stdout { pipe("/dev/stdout"); }; +destination d_redis_ui_log { + redis( + host("`REDIS_SLAVEOF_IP`") + persist-name("redis1") + port(`REDIS_SLAVEOF_PORT`) + auth("`REDISPASS`") + command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n") + ); +}; +destination d_redis_f2b_channel { + redis( + host("`REDIS_SLAVEOF_IP`") + persist-name("redis2") + port(`REDIS_SLAVEOF_PORT`) + auth("`REDISPASS`") + command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)") + ); +}; +filter f_mail { facility(mail); }; +# start +# overriding warnings are still displayed when the entrypoint runs its initial check +# warnings logged by postfix-mailcow to syslog are hidden to reduce repeating msgs +# Some other warnings are ignored +filter f_ignore { + not match("overriding earlier entry" value("MESSAGE")); + not match("TLS SNI from checks.mailcow.email" value("MESSAGE")); + not match("no SASL support" value("MESSAGE")); + not facility (local0, local1, local2, local3, local4, local5, local6, local7); +}; +# end +log { + source(s_src); + filter(f_ignore); + destination(d_stdout); + filter(f_mail); + destination(d_redis_ui_log); + destination(d_redis_f2b_channel); +}; diff --git a/mailcow/data/Dockerfiles/postfix/syslog-ng.conf b/mailcow/data/Dockerfiles/postfix/syslog-ng.conf new file mode 100644 index 0000000..fc7d1aa --- /dev/null +++ b/mailcow/data/Dockerfiles/postfix/syslog-ng.conf @@ -0,0 +1,55 @@ +@version: 3.38 +@include "scl.conf" +options { + chain_hostnames(off); + flush_lines(0); + use_dns(no); + dns_cache(no); + use_fqdn(no); + owner("root"); group("adm"); perm(0640); + stats_freq(0); + bad_hostname("^gconfd$"); +}; +source s_src { + unix-stream("/dev/log"); + internal(); +}; +destination d_stdout { pipe("/dev/stdout"); }; +destination d_redis_ui_log { + redis( + host("redis-mailcow") + persist-name("redis1") + port(6379) + auth("`REDISPASS`") + command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n") + ); +}; +destination d_redis_f2b_channel { + redis( + host("redis-mailcow") + persist-name("redis2") + port(6379) + auth("`REDISPASS`") + command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)") + ); +}; +filter f_mail { facility(mail); }; +# start +# overriding warnings are still displayed when the entrypoint runs its initial check +# warnings logged by postfix-mailcow to syslog are hidden to reduce repeating msgs +# Some other warnings are ignored +filter f_ignore { + not match("overriding earlier entry" value("MESSAGE")); + not match("TLS SNI from checks.mailcow.email" value("MESSAGE")); + not match("no SASL support" value("MESSAGE")); + not facility (local0, local1, local2, local3, local4, local5, local6, local7); +}; +# end +log { + source(s_src); + filter(f_ignore); + destination(d_stdout); + filter(f_mail); + destination(d_redis_ui_log); + destination(d_redis_f2b_channel); +}; diff --git a/mailcow/data/Dockerfiles/postfix/whitelist_forwardinghosts.sh b/mailcow/data/Dockerfiles/postfix/whitelist_forwardinghosts.sh new file mode 100755 index 0000000..4ad5ab3 --- /dev/null +++ b/mailcow/data/Dockerfiles/postfix/whitelist_forwardinghosts.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +while read QUERY; do + QUERY=($QUERY) + if [ "${QUERY[0]}" != "get" ]; then + echo "500 dunno" + continue + fi + result=$(curl -s http://nginx:8081/forwardinghosts.php?host=${QUERY[1]}) + logger -t whitelist_forwardinghosts -p mail.info "Look up ${QUERY[1]} on whitelist, result $result" + echo ${result} +done diff --git a/mailcow/data/Dockerfiles/rspamd/Dockerfile b/mailcow/data/Dockerfiles/rspamd/Dockerfile new file mode 100644 index 0000000..68b38d3 --- /dev/null +++ b/mailcow/data/Dockerfiles/rspamd/Dockerfile @@ -0,0 +1,40 @@ +FROM debian:bookworm-slim +LABEL maintainer="The Infrastructure Company GmbH " + +ARG DEBIAN_FRONTEND=noninteractive +ARG RSPAMD_VER=rspamd_3.11.1-1~ab0b44951 +ARG CODENAME=bookworm +ENV LC_ALL=C + +RUN apt-get update && apt-get install -y --no-install-recommends \ + tzdata \ + ca-certificates \ + gnupg2 \ + apt-transport-https \ + dnsutils \ + netcat-traditional \ + wget \ + redis-tools \ + procps \ + nano \ + lua-cjson \ + && arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \ + && wget -P /tmp https://rspamd.com/apt-stable/pool/main/r/rspamd/${RSPAMD_VER}~${CODENAME}_${arch}.deb\ + && apt install -y /tmp/${RSPAMD_VER}~${CODENAME}_${arch}.deb \ + && rm -rf /var/lib/apt/lists/* /tmp/*\ + && apt-get autoremove --purge \ + && apt-get clean \ + && mkdir -p /run/rspamd \ + && chown _rspamd:_rspamd /run/rspamd \ + && echo 'alias ll="ls -la --color"' >> ~/.bashrc \ + && sed -i 's/#analysis_keyword_table > 0/analysis_cat_table.macro_exist == "M"/g' /usr/share/rspamd/lualib/lua_scanners/oletools.lua + +COPY settings.conf /etc/rspamd/settings.conf +COPY set_worker_password.sh /set_worker_password.sh +COPY docker-entrypoint.sh /docker-entrypoint.sh + +ENTRYPOINT ["/docker-entrypoint.sh"] + +STOPSIGNAL SIGTERM + +CMD ["/usr/bin/rspamd", "-f", "-u", "_rspamd", "-g", "_rspamd"] diff --git a/mailcow/data/Dockerfiles/rspamd/docker-entrypoint.sh b/mailcow/data/Dockerfiles/rspamd/docker-entrypoint.sh new file mode 100755 index 0000000..cf44c30 --- /dev/null +++ b/mailcow/data/Dockerfiles/rspamd/docker-entrypoint.sh @@ -0,0 +1,315 @@ +#!/bin/bash + +until nc phpfpm 9001 -z; do + echo "Waiting for PHP on port 9001..." + sleep 3 +done + +until nc phpfpm 9002 -z; do + echo "Waiting for PHP on port 9002..." + sleep 3 +done + +mkdir -p /etc/rspamd/plugins.d \ + /etc/rspamd/custom + +touch /etc/rspamd/rspamd.conf.local \ + /etc/rspamd/rspamd.conf.override + +chmod 755 /var/lib/rspamd + + +[[ ! -f /etc/rspamd/override.d/worker-controller-password.inc ]] && echo '# Autogenerated by mailcow' > /etc/rspamd/override.d/worker-controller-password.inc + +echo ${IPV4_NETWORK}.0/24 > /etc/rspamd/custom/mailcow_networks.map +echo ${IPV6_NETWORK} >> /etc/rspamd/custom/mailcow_networks.map + +DOVECOT_V4= +DOVECOT_V6= +until [[ ! -z ${DOVECOT_V4} ]]; do + DOVECOT_V4=$(dig a dovecot +short) + DOVECOT_V6=$(dig aaaa dovecot +short) + [[ ! -z ${DOVECOT_V4} ]] && break; + echo "Waiting for Dovecot..." + sleep 3 +done +echo ${DOVECOT_V4}/32 > /etc/rspamd/custom/dovecot_trusted.map +if [[ ! -z ${DOVECOT_V6} ]]; then + echo ${DOVECOT_V6}/128 >> /etc/rspamd/custom/dovecot_trusted.map +fi + +RSPAMD_V4= +RSPAMD_V6= +until [[ ! -z ${RSPAMD_V4} ]]; do + RSPAMD_V4=$(dig a rspamd +short) + RSPAMD_V6=$(dig aaaa rspamd +short) + [[ ! -z ${RSPAMD_V4} ]] && break; + echo "Waiting for Rspamd..." + sleep 3 +done +echo ${RSPAMD_V4}/32 > /etc/rspamd/custom/rspamd_trusted.map +if [[ ! -z ${RSPAMD_V6} ]]; then + echo ${RSPAMD_V6}/128 >> /etc/rspamd/custom/rspamd_trusted.map +fi + +if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then + cat < /etc/rspamd/local.d/redis.conf +read_servers = "redis:6379"; +write_servers = "${REDIS_SLAVEOF_IP}:${REDIS_SLAVEOF_PORT}"; +password = "${REDISPASS}"; +timeout = 10; +EOF + until [[ $(redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning PING) == "PONG" ]]; do + echo "Waiting for Redis @redis-mailcow..." + sleep 2 + done + until [[ $(redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning PING) == "PONG" ]]; do + echo "Waiting for Redis @${REDIS_SLAVEOF_IP}..." + sleep 2 + done + redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning SLAVEOF ${REDIS_SLAVEOF_IP} ${REDIS_SLAVEOF_PORT} +else + cat < /etc/rspamd/local.d/redis.conf +servers = "redis:6379"; +password = "${REDISPASS}"; +timeout = 10; +EOF + until [[ $(redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning PING) == "PONG" ]]; do + echo "Waiting for Redis slave..." + sleep 2 + done + redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning SLAVEOF NO ONE +fi + +# Provide additional lua modules +ln -s /usr/lib/$(uname -m)-linux-gnu/liblua5.1-cjson.so.0.0.0 /usr/lib/rspamd/cjson.so + +chown -R _rspamd:_rspamd /var/lib/rspamd \ + /etc/rspamd/local.d \ + /etc/rspamd/override.d \ + /etc/rspamd/rspamd.conf.local \ + /etc/rspamd/rspamd.conf.override \ + /etc/rspamd/plugins.d + +# Fix missing default global maps, if any +# These exists in mailcow UI and should not be removed +touch /etc/rspamd/custom/global_mime_from_blacklist.map \ + /etc/rspamd/custom/global_rcpt_blacklist.map \ + /etc/rspamd/custom/global_smtp_from_blacklist.map \ + /etc/rspamd/custom/global_mime_from_whitelist.map \ + /etc/rspamd/custom/global_rcpt_whitelist.map \ + /etc/rspamd/custom/global_smtp_from_whitelist.map \ + /etc/rspamd/custom/bad_languages.map \ + /etc/rspamd/custom/sa-rules \ + /etc/rspamd/custom/dovecot_trusted.map \ + /etc/rspamd/custom/rspamd_trusted.map \ + /etc/rspamd/custom/mailcow_networks.map \ + /etc/rspamd/custom/ip_wl.map \ + /etc/rspamd/custom/fishy_tlds.map \ + /etc/rspamd/custom/bad_words.map \ + /etc/rspamd/custom/bad_asn.map \ + /etc/rspamd/custom/bad_words_de.map \ + /etc/rspamd/custom/bulk_header.map \ + /etc/rspamd/custom/bad_header.map + +# www-data (82) group needs to write to these files +chown _rspamd:_rspamd /etc/rspamd/custom/ +chmod 0755 /etc/rspamd/custom/. +chown -R 82:82 /etc/rspamd/custom/* +chmod 644 -R /etc/rspamd/custom/* + +# Run hooks +for file in /hooks/*; do + if [ -x "${file}" ]; then + echo "Running hook ${file}" + "${file}" + fi +done + +# If DQS KEY is set in mailcow.conf add Spamhaus DQS RBLs +if [[ ! -z ${SPAMHAUS_DQS_KEY} ]]; then + cat < /etc/rspamd/custom/dqs-rbl.conf + # Autogenerated by mailcow. DO NOT TOUCH! + spamhaus { + rbl = "${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net"; + from = false; + } + spamhaus_from { + from = true; + received = false; + rbl = "${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net"; + returncodes { + SPAMHAUS_ZEN = [ "127.0.0.2", "127.0.0.3", "127.0.0.4", "127.0.0.5", "127.0.0.6", "127.0.0.7", "127.0.0.9", "127.0.0.10", "127.0.0.11" ]; + } + } + spamhaus_authbl_received { + # Check if the sender client is listed in AuthBL (AuthBL is *not* part of ZEN) + rbl = "${SPAMHAUS_DQS_KEY}.authbl.dq.spamhaus.net"; + from = false; + received = true; + ipv6 = true; + returncodes { + SH_AUTHBL_RECEIVED = "127.0.0.20" + } + } + spamhaus_dbl { + # Add checks on the HELO string + rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net"; + helo = true; + rdns = true; + dkim = true; + disable_monitoring = true; + returncodes { + RBL_DBL_SPAM = "127.0.1.2"; + RBL_DBL_PHISH = "127.0.1.4"; + RBL_DBL_MALWARE = "127.0.1.5"; + RBL_DBL_BOTNET = "127.0.1.6"; + RBL_DBL_ABUSED_SPAM = "127.0.1.102"; + RBL_DBL_ABUSED_PHISH = "127.0.1.104"; + RBL_DBL_ABUSED_MALWARE = "127.0.1.105"; + RBL_DBL_ABUSED_BOTNET = "127.0.1.106"; + RBL_DBL_DONT_QUERY_IPS = "127.0.1.255"; + } + } + spamhaus_dbl_fullurls { + ignore_defaults = true; + no_ip = true; + rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net"; + selector = 'urls:get_host' + disable_monitoring = true; + returncodes { + DBLABUSED_SPAM_FULLURLS = "127.0.1.102"; + DBLABUSED_PHISH_FULLURLS = "127.0.1.104"; + DBLABUSED_MALWARE_FULLURLS = "127.0.1.105"; + DBLABUSED_BOTNET_FULLURLS = "127.0.1.106"; + } + } + spamhaus_zrd { + # Add checks on the HELO string also for DQS + rbl = "${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net"; + helo = true; + rdns = true; + dkim = true; + disable_monitoring = true; + returncodes { + RBL_ZRD_VERY_FRESH_DOMAIN = ["127.0.2.2", "127.0.2.3", "127.0.2.4"]; + RBL_ZRD_FRESH_DOMAIN = [ + "127.0.2.5", "127.0.2.6", "127.0.2.7", "127.0.2.8", "127.0.2.9", "127.0.2.10", "127.0.2.11", "127.0.2.12", "127.0.2.13", "127.0.2.14", "127.0.2.15", "127.0.2.16", "127.0.2.17", "127.0.2.18", "127.0.2.19", "127.0.2.20", "127.0.2.21", "127.0.2.22", "127.0.2.23", "127.0.2.24" + ]; + RBL_ZRD_DONT_QUERY_IPS = "127.0.2.255"; + } + } + "SPAMHAUS_ZEN_URIBL" { + enabled = true; + rbl = "${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net"; + resolve_ip = true; + checks = ['urls']; + replyto = true; + emails = true; + ipv4 = true; + ipv6 = true; + emails_domainonly = true; + returncodes { + URIBL_SBL = "127.0.0.2"; + URIBL_SBL_CSS = "127.0.0.3"; + URIBL_XBL = ["127.0.0.4", "127.0.0.5", "127.0.0.6", "127.0.0.7"]; + URIBL_PBL = ["127.0.0.10", "127.0.0.11"]; + URIBL_DROP = "127.0.0.9"; + } + } + SH_EMAIL_DBL { + ignore_defaults = true; + replyto = true; + emails_domainonly = true; + disable_monitoring = true; + rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net"; + returncodes = { + SH_EMAIL_DBL = [ + "127.0.1.2", + "127.0.1.4", + "127.0.1.5", + "127.0.1.6" + ]; + SH_EMAIL_DBL_ABUSED = [ + "127.0.1.102", + "127.0.1.104", + "127.0.1.105", + "127.0.1.106" + ]; + SH_EMAIL_DBL_DONT_QUERY_IPS = [ "127.0.1.255" ]; + } + } + SH_EMAIL_ZRD { + ignore_defaults = true; + replyto = true; + emails_domainonly = true; + disable_monitoring = true; + rbl = "${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net"; + returncodes = { + SH_EMAIL_ZRD_VERY_FRESH_DOMAIN = ["127.0.2.2", "127.0.2.3", "127.0.2.4"]; + SH_EMAIL_ZRD_FRESH_DOMAIN = [ + "127.0.2.5", "127.0.2.6", "127.0.2.7", "127.0.2.8", "127.0.2.9", "127.0.2.10", "127.0.2.11", "127.0.2.12", "127.0.2.13", "127.0.2.14", "127.0.2.15", "127.0.2.16", "127.0.2.17", "127.0.2.18", "127.0.2.19", "127.0.2.20", "127.0.2.21", "127.0.2.22", "127.0.2.23", "127.0.2.24" + ]; + SH_EMAIL_ZRD_DONT_QUERY_IPS = [ "127.0.2.255" ]; + } + } + "DBL" { + # override the defaults for DBL defined in modules.d/rbl.conf + rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net"; + disable_monitoring = true; + } + "ZRD" { + ignore_defaults = true; + rbl = "${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net"; + no_ip = true; + dkim = true; + emails = true; + emails_domainonly = true; + urls = true; + returncodes = { + ZRD_VERY_FRESH_DOMAIN = ["127.0.2.2", "127.0.2.3", "127.0.2.4"]; + ZRD_FRESH_DOMAIN = ["127.0.2.5", "127.0.2.6", "127.0.2.7", "127.0.2.8", "127.0.2.9", "127.0.2.10", "127.0.2.11", "127.0.2.12", "127.0.2.13", "127.0.2.14", "127.0.2.15", "127.0.2.16", "127.0.2.17", "127.0.2.18", "127.0.2.19", "127.0.2.20", "127.0.2.21", "127.0.2.22", "127.0.2.23", "127.0.2.24"]; + } + } + spamhaus_sbl_url { + ignore_defaults = true + rbl = "${SPAMHAUS_DQS_KEY}.sbl.dq.spamhaus.net"; + checks = ['urls']; + disable_monitoring = true; + returncodes { + SPAMHAUS_SBL_URL = "127.0.0.2"; + } + } + + SH_HBL_EMAIL { + ignore_defaults = true; + rbl = "_email.${SPAMHAUS_DQS_KEY}.hbl.dq.spamhaus.net"; + emails_domainonly = false; + selector = "from('smtp').lower;from('mime').lower"; + ignore_whitelist = true; + checks = ['emails', 'replyto']; + hash = "sha1"; + returncodes = { + SH_HBL_EMAIL = [ + "127.0.3.2" + ]; + } + } + + spamhaus_dqs_hbl { + symbol = "HBL_FILE_UNKNOWN"; + rbl = "_file.${SPAMHAUS_DQS_KEY}.hbl.dq.spamhaus.net."; + selector = "attachments('rbase32', 'sha256')"; + ignore_whitelist = true; + ignore_defaults = true; + returncodes { + SH_HBL_FILE_MALICIOUS = "127.0.3.10"; + SH_HBL_FILE_SUSPICIOUS = "127.0.3.15"; + } + } +EOF +else + rm -rf /etc/rspamd/custom/dqs-rbl.conf +fi + +exec "$@" diff --git a/mailcow/data/Dockerfiles/rspamd/sa_trivial_convert.lua b/mailcow/data/Dockerfiles/rspamd/sa_trivial_convert.lua new file mode 100644 index 0000000..4725dab --- /dev/null +++ b/mailcow/data/Dockerfiles/rspamd/sa_trivial_convert.lua @@ -0,0 +1,443 @@ +local fun = require "fun" +local rspamd_logger = require "rspamd_logger" +local util = require "rspamd_util" +local lua_util = require "lua_util" +local rspamd_regexp = require "rspamd_regexp" +local ucl = require "ucl" + +local complicated = {} +local rules = {} +local scores = {} + +local function words_to_re(words, start) + return table.concat(fun.totable(fun.drop_n(start, words)), " "); +end + +local function split(str, delim) + local result = {} + + if not delim then + delim = '[^%s]+' + end + + for token in string.gmatch(str, delim) do + table.insert(result, token) + end + + return result +end + +local function handle_header_def(hline, cur_rule) + --Now check for modifiers inside header's name + local hdrs = split(hline, '[^|]+') + local hdr_params = {} + local cur_param = {} + -- Check if an re is an ordinary re + local ordinary = true + + for _,h in ipairs(hdrs) do + if h == 'ALL' or h == 'ALL:raw' then + ordinary = false + else + local args = split(h, '[^:]+') + cur_param['strong'] = false + cur_param['raw'] = false + cur_param['header'] = args[1] + + if args[2] then + -- We have some ops that are required for the header, so it's not ordinary + ordinary = false + end + + fun.each(function(func) + if func == 'addr' then + cur_param['function'] = function(str) + local addr_parsed = util.parse_addr(str) + local ret = {} + if addr_parsed then + for _,elt in ipairs(addr_parsed) do + if elt['addr'] then + table.insert(ret, elt['addr']) + end + end + end + + return ret + end + elseif func == 'name' then + cur_param['function'] = function(str) + local addr_parsed = util.parse_addr(str) + local ret = {} + if addr_parsed then + for _,elt in ipairs(addr_parsed) do + if elt['name'] then + table.insert(ret, elt['name']) + end + end + end + + return ret + end + elseif func == 'raw' then + cur_param['raw'] = true + elseif func == 'case' then + cur_param['strong'] = true + else + rspamd_logger.warnx(rspamd_config, 'Function %1 is not supported in %2', + func, cur_rule['symbol']) + end + end, fun.tail(args)) + + -- Some header rules require splitting to check of multiple headers + if cur_param['header'] == 'MESSAGEID' then + -- Special case for spamassassin + ordinary = false + elseif cur_param['header'] == 'ToCc' then + ordinary = false + else + table.insert(hdr_params, cur_param) + end + end + + cur_rule['ordinary'] = ordinary and (not (#hdr_params > 1)) + cur_rule['header'] = hdr_params + end +end + +local function process_sa_conf(f) + local cur_rule = {} + local valid_rule = false + + local function insert_cur_rule() + if not rules[cur_rule.type] then + rules[cur_rule.type] = {} + end + + local target = rules[cur_rule.type] + + if cur_rule.type == 'header' then + if not cur_rule.header[1].header then + rspamd_logger.errx(rspamd_config, 'bad rule definition: %1', cur_rule) + return + end + if not target[cur_rule.header[1].header] then + target[cur_rule.header[1].header] = {} + end + target = target[cur_rule.header[1].header] + end + + if not cur_rule['symbol'] then + rspamd_logger.errx(rspamd_config, 'bad rule definition: %1', cur_rule) + return + end + target[cur_rule['symbol']] = cur_rule + cur_rule = {} + valid_rule = false + end + + local function parse_score(words) + if #words == 3 then + -- score rule + return tonumber(words[3]) + elseif #words == 6 then + -- score rule + -- we assume here that bayes and network are enabled and select + return tonumber(words[6]) + else + rspamd_logger.errx(rspamd_config, 'invalid score for %1', words[2]) + end + + return 0 + end + + local skip_to_endif = false + local if_nested = 0 + for l in f:lines() do + (function () + l = lua_util.rspamd_str_trim(l) + -- Replace bla=~/re/ with bla =~ /re/ (#2372) + l = l:gsub('([^%s])%s*([=!]~)%s*([^%s])', '%1 %2 %3') + + if string.len(l) == 0 or string.sub(l, 1, 1) == '#' then + return + end + + -- Unbalanced if/endif + if if_nested < 0 then if_nested = 0 end + if skip_to_endif then + if string.match(l, '^endif') then + if_nested = if_nested - 1 + + if if_nested == 0 then + skip_to_endif = false + end + elseif string.match(l, '^if') then + if_nested = if_nested + 1 + elseif string.match(l, '^else') then + -- Else counterpart for if + skip_to_endif = false + end + table.insert(complicated, l) + return + else + if string.match(l, '^ifplugin') then + skip_to_endif = true + if_nested = if_nested + 1 + table.insert(complicated, l) + elseif string.match(l, '^if !plugin%(') then + skip_to_endif = true + if_nested = if_nested + 1 + table.insert(complicated, l) + elseif string.match(l, '^if') then + -- Unknown if + skip_to_endif = true + if_nested = if_nested + 1 + table.insert(complicated, l) + elseif string.match(l, '^else') then + -- Else counterpart for if + skip_to_endif = true + table.insert(complicated, l) + elseif string.match(l, '^endif') then + if_nested = if_nested - 1 + table.insert(complicated, l) + end + end + + -- Skip comments + local words = fun.totable(fun.take_while( + function(w) return string.sub(w, 1, 1) ~= '#' end, + fun.filter(function(w) + return w ~= "" end, + fun.iter(split(l))))) + + if words[1] == "header" then + -- header SYMBOL Header ~= /regexp/ + if valid_rule then + insert_cur_rule() + end + if words[4] and (words[4] == '=~' or words[4] == '!~') then + cur_rule['type'] = 'header' + cur_rule['symbol'] = words[2] + + if words[4] == '!~' then + table.insert(complicated, l) + return + end + + cur_rule['re_expr'] = words_to_re(words, 4) + local unset_comp = string.find(cur_rule['re_expr'], '%s+%[if%-unset:') + if unset_comp then + table.insert(complicated, l) + return + end + + cur_rule['re'] = rspamd_regexp.create(cur_rule['re_expr']) + + if not cur_rule['re'] then + rspamd_logger.warnx(rspamd_config, "Cannot parse regexp '%1' for %2", + cur_rule['re_expr'], cur_rule['symbol']) + table.insert(complicated, l) + return + else + handle_header_def(words[3], cur_rule) + if not cur_rule['ordinary'] then + table.insert(complicated, l) + return + end + end + + valid_rule = true + else + table.insert(complicated, l) + return + end + elseif words[1] == "body" then + -- body SYMBOL /regexp/ + if valid_rule then + insert_cur_rule() + end + + cur_rule['symbol'] = words[2] + if words[3] and (string.sub(words[3], 1, 1) == '/' + or string.sub(words[3], 1, 1) == 'm') then + cur_rule['type'] = 'sabody' + cur_rule['re_expr'] = words_to_re(words, 2) + cur_rule['re'] = rspamd_regexp.create(cur_rule['re_expr']) + if cur_rule['re'] then + + valid_rule = true + end + else + -- might be function + table.insert(complicated, l) + return + end + elseif words[1] == "rawbody" then + -- body SYMBOL /regexp/ + if valid_rule then + insert_cur_rule() + end + + cur_rule['symbol'] = words[2] + if words[3] and (string.sub(words[3], 1, 1) == '/' + or string.sub(words[3], 1, 1) == 'm') then + cur_rule['type'] = 'sarawbody' + cur_rule['re_expr'] = words_to_re(words, 2) + cur_rule['re'] = rspamd_regexp.create(cur_rule['re_expr']) + if cur_rule['re'] then + valid_rule = true + end + else + table.insert(complicated, l) + return + end + elseif words[1] == "full" then + -- body SYMBOL /regexp/ + if valid_rule then + insert_cur_rule() + end + + cur_rule['symbol'] = words[2] + + if words[3] and (string.sub(words[3], 1, 1) == '/' + or string.sub(words[3], 1, 1) == 'm') then + cur_rule['type'] = 'message' + cur_rule['re_expr'] = words_to_re(words, 2) + cur_rule['re'] = rspamd_regexp.create(cur_rule['re_expr']) + cur_rule['raw'] = true + if cur_rule['re'] then + valid_rule = true + end + else + table.insert(complicated, l) + return + end + elseif words[1] == "uri" then + -- uri SYMBOL /regexp/ + if valid_rule then + insert_cur_rule() + end + cur_rule['type'] = 'uri' + cur_rule['symbol'] = words[2] + cur_rule['re_expr'] = words_to_re(words, 2) + cur_rule['re'] = rspamd_regexp.create(cur_rule['re_expr']) + if cur_rule['re'] and cur_rule['symbol'] then + valid_rule = true + else + table.insert(complicated, l) + return + end + elseif words[1] == "meta" then + -- meta SYMBOL expression + if valid_rule then + insert_cur_rule() + end + table.insert(complicated, l) + return + elseif words[1] == "describe" and valid_rule then + cur_rule['description'] = words_to_re(words, 2) + elseif words[1] == "score" then + scores[words[2]] = parse_score(words) + else + table.insert(complicated, l) + return + end + end)() + end + if valid_rule then + insert_cur_rule() + end +end + +for _,matched in ipairs(arg) do + local f = io.open(matched, "r") + if f then + rspamd_logger.messagex(rspamd_config, 'loading SA rules from %s', matched) + process_sa_conf(f) + else + rspamd_logger.errx(rspamd_config, "cannot open %1", matched) + end +end + +local multimap_conf = {} + +local function handle_rule(what, syms, hdr) + local mtype + local filter + local fname + local header + local sym = what:upper() + if what == 'sabody' then + mtype = 'content' + fname = 'body_re.map' + filter = 'oneline' + elseif what == 'sarawbody' then + fname = 'raw_body_re.map' + mtype = 'content' + filter = 'rawtext' + elseif what == 'full' then + fname = 'full_re.map' + mtype = 'content' + filter = 'full' + elseif what == 'uri' then + fname = 'uri_re.map' + mtype = 'url' + filter = 'full' + elseif what == 'header' then + fname = ('hdr_' .. hdr .. '_re.map'):lower() + mtype = 'header' + header = hdr + sym = sym .. '_' .. hdr:upper() + else + rspamd_logger.errx('unknown type: %s', what) + return + end + local conf = { + type = mtype, + filter = filter, + symbol = 'SA_MAP_AUTO_' .. sym, + regexp = true, + map = fname, + header = header, + symbols = {} + } + local re_file = io.open(fname, 'w') + + for k,r in pairs(syms) do + local score = 0.0 + if scores[k] then + score = scores[k] + end + re_file:write(string.format('/%s/ %s:%f\n', tostring(r.re), k, score)) + table.insert(conf.symbols, k) + end + + re_file:close() + + multimap_conf[sym:lower()] = conf + rspamd_logger.messagex('stored %s regexp in %s', sym:lower(), fname) +end + +for k,v in pairs(rules) do + if k == 'header' then + for h,r in pairs(v) do + handle_rule(k, r, h) + end + else + handle_rule(k, v) + end +end + +local out = ucl.to_format(multimap_conf, 'ucl') +local mmap_conf = io.open('auto_multimap.conf', 'w') +mmap_conf:write(out) +mmap_conf:close() +rspamd_logger.messagex('stored multimap conf in %s', 'auto_multimap.conf') + +local sa_remain = io.open('auto_sa.conf', 'w') +fun.each(function(l) + sa_remain:write(l) + sa_remain:write('\n') +end, fun.filter(function(l) return not string.match(l, '^%s+$') end, complicated)) +sa_remain:close() +rspamd_logger.messagex('stored sa remains conf in %s', 'auto_sa.conf') diff --git a/mailcow/data/Dockerfiles/rspamd/set_worker_password.sh b/mailcow/data/Dockerfiles/rspamd/set_worker_password.sh new file mode 100755 index 0000000..7205e88 --- /dev/null +++ b/mailcow/data/Dockerfiles/rspamd/set_worker_password.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +password_file='/etc/rspamd/override.d/worker-controller-password.inc' +password_hash=`/usr/bin/rspamadm pw -e -p $1` + +echo 'enable_password = "'$password_hash'";' > $password_file + +if grep -q "$password_hash" "$password_file"; then + echo "OK" +else + echo "ERROR" +fi \ No newline at end of file diff --git a/mailcow/data/Dockerfiles/rspamd/settings.conf b/mailcow/data/Dockerfiles/rspamd/settings.conf new file mode 100644 index 0000000..4449f09 --- /dev/null +++ b/mailcow/data/Dockerfiles/rspamd/settings.conf @@ -0,0 +1 @@ +settings = "http://nginx:8081/settings.php"; diff --git a/mailcow/data/Dockerfiles/sogo/Dockerfile b/mailcow/data/Dockerfiles/sogo/Dockerfile new file mode 100644 index 0000000..f2981ad --- /dev/null +++ b/mailcow/data/Dockerfiles/sogo/Dockerfile @@ -0,0 +1,59 @@ +FROM debian:bookworm-slim + +LABEL maintainer="The Infrastructure Company GmbH " + +ARG DEBIAN_FRONTEND=noninteractive +ARG DEBIAN_VERSION=bookworm +ARG SOGO_DEBIAN_REPOSITORY=https://packagingv2.sogo.nu/sogo-nightly-debian/ +# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?.*)$ +ARG GOSU_VERSION=1.17 +ENV LC_ALL=C + +# Prerequisites +RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \ + && apt-get update && apt-get install -y --no-install-recommends \ + apt-transport-https \ + ca-certificates \ + gettext \ + gnupg \ + mariadb-client \ + rsync \ + supervisor \ + syslog-ng \ + syslog-ng-core \ + syslog-ng-mod-redis \ + dirmngr \ + netcat-traditional \ + psmisc \ + wget \ + patch \ + && dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \ + && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \ + && chmod +x /usr/local/bin/gosu \ + && gosu nobody true \ + && mkdir /usr/share/doc/sogo \ + && touch /usr/share/doc/sogo/empty.sh \ + && wget -O- https://keys.openpgp.org/vks/v1/by-fingerprint/74FFC6D72B925A34B5D356BDF8A27B36A6E2EAE9 | gpg --dearmor | apt-key add - \ + && echo "deb [trusted=yes] ${SOGO_DEBIAN_REPOSITORY} ${DEBIAN_VERSION} main" > /etc/apt/sources.list.d/sogo.list \ + && apt-get update && apt-get install -y --no-install-recommends \ + sogo \ + sogo-activesync \ + && apt-get autoclean \ + && rm -rf /var/lib/apt/lists/* \ + && touch /etc/default/locale + +COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh +COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf +COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf +COPY supervisord.conf /etc/supervisor/supervisord.conf +COPY acl.diff /acl.diff +COPY navMailcowBtns.diff /navMailcowBtns.diff +COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh +COPY docker-entrypoint.sh / + +RUN chmod +x /bootstrap-sogo.sh \ + /usr/local/sbin/stop-supervisor.sh + +ENTRYPOINT ["/docker-entrypoint.sh"] + +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] \ No newline at end of file diff --git a/mailcow/data/Dockerfiles/sogo/acl.diff b/mailcow/data/Dockerfiles/sogo/acl.diff new file mode 100644 index 0000000..5137003 --- /dev/null +++ b/mailcow/data/Dockerfiles/sogo/acl.diff @@ -0,0 +1,11 @@ +--- /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:57.987504204 +0200 ++++ /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:35.918291298 +0200 +@@ -46,7 +46,7 @@ + + + +- + diff --git a/mailcow/data/Dockerfiles/sogo/bootstrap-sogo.sh b/mailcow/data/Dockerfiles/sogo/bootstrap-sogo.sh new file mode 100755 index 0000000..abd398b --- /dev/null +++ b/mailcow/data/Dockerfiles/sogo/bootstrap-sogo.sh @@ -0,0 +1,153 @@ +#!/bin/bash + +# Wait for MySQL to warm-up +while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do + echo "Waiting for database to come up..." + sleep 2 +done + +# Wait until port becomes free and send sig +until ! nc -z sogo-mailcow 20000; +do + killall -TERM sogod + sleep 3 +done + +# Wait for updated schema +DBV_NOW=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN) +DBV_NEW=$(grep -oE '\$db_version = .*;' init_db.inc.php | sed 's/$db_version = //g;s/;//g' | cut -d \" -f2) +while [[ "${DBV_NOW}" != "${DBV_NEW}" ]]; do + echo "Waiting for schema update..." + DBV_NOW=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN) + DBV_NEW=$(grep -oE '\$db_version = .*;' init_db.inc.php | sed 's/$db_version = //g;s/;//g' | cut -d \" -f2) + sleep 5 +done +echo "DB schema is ${DBV_NOW}" + +# cat /dev/urandom seems to hang here occasionally and is not recommended anyway, better use openssl +RAND_PASS=$(openssl rand -base64 16 | tr -dc _A-Z-a-z-0-9) + +# Generate plist header with timezone data +mkdir -p /var/lib/sogo/GNUstep/Defaults/ +cat < /var/lib/sogo/GNUstep/Defaults/sogod.plist + + + + + OCSAclURL + mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_acl + SOGoIMAPServer + imap://${IPV4_NETWORK}.250:143/?TLS=YES&tlsVerifyMode=none + SOGoSieveServer + sieve://${IPV4_NETWORK}.250:4190/?TLS=YES&tlsVerifyMode=none + SOGoSMTPServer + smtp://${IPV4_NETWORK}.253:588/?TLS=YES&tlsVerifyMode=none + SOGoTrustProxyAuthentication + YES + SOGoEncryptionKey + ${RAND_PASS} + OCSAdminURL + mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_admin + OCSCacheFolderURL + mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_cache_folder + OCSEMailAlarmsFolderURL + mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_alarms_folder + OCSFolderInfoURL + mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_folder_info + OCSSessionsFolderURL + mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_sessions_folder + OCSStoreURL + mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_store + SOGoProfileURL + mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_user_profile + SOGoTimeZone + ${TZ} + domains + +EOF + +# Generate multi-domain setup +while read -r line gal + do + echo " ${line} + + SOGoMailDomain + ${line} + SOGoUserSources + + + MailFieldNames + + aliases + ad_aliases + ext_acl + + KindFieldName + kind + DomainFieldName + domain + MultipleBookingsFieldName + multiple_bookings + listRequiresDot + NO + canAuthenticate + YES + displayName + GAL ${line} + id + ${line} + isAddressBook + ${gal} + type + sql + userPasswordAlgorithm + ${MAILCOW_PASS_SCHEME} + prependPasswordScheme + YES + viewURL + mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/_sogo_static_view + " >> /var/lib/sogo/GNUstep/Defaults/sogod.plist + # Generate alternative LDAP authentication dict, when SQL authentication fails + # This will nevertheless read attributes from LDAP + /etc/sogo/plist_ldap.sh ${line} ${gal} >> /var/lib/sogo/GNUstep/Defaults/sogod.plist + echo " + " >> /var/lib/sogo/GNUstep/Defaults/sogod.plist +done < <(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain, CASE gal WHEN '1' THEN 'YES' ELSE 'NO' END AS gal FROM domain;" -B -N) + +# Generate footer +echo ' + +' >> /var/lib/sogo/GNUstep/Defaults/sogod.plist + +# Fix permissions +chown sogo:sogo -R /var/lib/sogo/ +chmod 600 /var/lib/sogo/GNUstep/Defaults/sogod.plist + +# Patch ACLs +#if [[ ${ACL_ANYONE} == 'allow' ]]; then +# #enable any or authenticated targets for ACL +# if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then +# patch -R /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff; +# fi +#else +# #disable any or authenticated targets for ACL +# if patch -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then +# patch /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff; +# fi +#fi + +if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff > /dev/null; then + patch -R /usr/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff; +fi + +# Rename custom logo, if any +[[ -f /etc/sogo/sogo-full.svg ]] && mv /etc/sogo/sogo-full.svg /etc/sogo/custom-fulllogo.svg + +# Rsync web content +echo "Syncing web content with named volume" +rsync -a /usr/lib/GNUstep/SOGo/. /sogo_web/ + +# Chown backup path +chown -R sogo:sogo /sogo_backup + +exec gosu sogo /usr/sbin/sogod diff --git a/mailcow/data/Dockerfiles/sogo/docker-entrypoint.sh b/mailcow/data/Dockerfiles/sogo/docker-entrypoint.sh new file mode 100755 index 0000000..4e20e96 --- /dev/null +++ b/mailcow/data/Dockerfiles/sogo/docker-entrypoint.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +if [[ "${SKIP_SOGO}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + echo "SKIP_SOGO=y, skipping SOGo..." + sleep 365d + exit 0 +fi + +if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then + cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf +fi + +echo "$TZ" > /etc/timezone + +# Run hooks +for file in /hooks/*; do + if [ -x "${file}" ]; then + echo "Running hook ${file}" + "${file}" + fi +done + +exec "$@" diff --git a/mailcow/data/Dockerfiles/sogo/navMailcowBtns.diff b/mailcow/data/Dockerfiles/sogo/navMailcowBtns.diff new file mode 100644 index 0000000..1b469aa --- /dev/null +++ b/mailcow/data/Dockerfiles/sogo/navMailcowBtns.diff @@ -0,0 +1,20 @@ +59,65d58 +< ng-show="::!activeUser.isSuperUser" +< var:ng-click="navButtonClick" +< ng-href="/user"> +< build +< +< +< ng-show="::activeUser.path.logoff.length" +85c78 +< ng-href="#"> +--- +> ng-href="{{::activeUser.path.logoff}}"> +89,91d81 +<
+< +< diff --git a/mailcow/data/Dockerfiles/sogo/stop-supervisor.sh b/mailcow/data/Dockerfiles/sogo/stop-supervisor.sh new file mode 100755 index 0000000..5394490 --- /dev/null +++ b/mailcow/data/Dockerfiles/sogo/stop-supervisor.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +printf "READY\n"; + +while read line; do + echo "Processing Event: $line" >&2; + kill -3 $(cat "/var/run/supervisord.pid") +done < /dev/stdin diff --git a/mailcow/data/Dockerfiles/sogo/supervisord.conf b/mailcow/data/Dockerfiles/sogo/supervisord.conf new file mode 100644 index 0000000..4946d98 --- /dev/null +++ b/mailcow/data/Dockerfiles/sogo/supervisord.conf @@ -0,0 +1,27 @@ +[supervisord] +nodaemon=true +user=root + +[program:syslog-ng] +command=/usr/sbin/syslog-ng --foreground --no-caps +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autostart=true +priority=1 + +[program:bootstrap-sogo] +command=/bootstrap-sogo.sh +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +priority=2 +startretries=10 +autorestart=true +stopwaitsecs=120 + +[eventlistener:processes] +command=/usr/local/sbin/stop-supervisor.sh +events=PROCESS_STATE_STOPPED, PROCESS_STATE_EXITED, PROCESS_STATE_FATAL diff --git a/mailcow/data/Dockerfiles/sogo/syslog-ng-redis_slave.conf b/mailcow/data/Dockerfiles/sogo/syslog-ng-redis_slave.conf new file mode 100644 index 0000000..675e4c6 --- /dev/null +++ b/mailcow/data/Dockerfiles/sogo/syslog-ng-redis_slave.conf @@ -0,0 +1,47 @@ +@version: 3.38 +@include "scl.conf" +options { + chain_hostnames(off); + flush_lines(0); + use_dns(no); + use_fqdn(no); + owner("root"); group("adm"); perm(0640); + stats_freq(0); + bad_hostname("^gconfd$"); +}; +source s_src { + unix-stream("/dev/log"); + internal(); +}; +source s_sogo { + pipe("/dev/sogo_log" owner(sogo) group(sogo)); +}; +destination d_stdout { pipe("/dev/stdout"); }; +destination d_redis_ui_log { + redis( + host("`REDIS_SLAVEOF_IP`") + persist-name("redis1") + port(`REDIS_SLAVEOF_PORT`) + auth("`REDISPASS`") + command("LPUSH" "SOGO_LOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n") + ); +}; +destination d_redis_f2b_channel { + redis( + host("`REDIS_SLAVEOF_IP`") + persist-name("redis2") + port(`REDIS_SLAVEOF_PORT`) + auth("`REDISPASS`") + command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)") + ); +}; +log { + source(s_sogo); + destination(d_redis_ui_log); + destination(d_redis_f2b_channel); +}; +log { + source(s_sogo); + source(s_src); + destination(d_stdout); +}; diff --git a/mailcow/data/Dockerfiles/sogo/syslog-ng.conf b/mailcow/data/Dockerfiles/sogo/syslog-ng.conf new file mode 100644 index 0000000..8460f2f --- /dev/null +++ b/mailcow/data/Dockerfiles/sogo/syslog-ng.conf @@ -0,0 +1,47 @@ +@version: 3.38 +@include "scl.conf" +options { + chain_hostnames(off); + flush_lines(0); + use_dns(no); + use_fqdn(no); + owner("root"); group("adm"); perm(0640); + stats_freq(0); + bad_hostname("^gconfd$"); +}; +source s_src { + unix-stream("/dev/log"); + internal(); +}; +source s_sogo { + pipe("/dev/sogo_log" owner(sogo) group(sogo)); +}; +destination d_stdout { pipe("/dev/stdout"); }; +destination d_redis_ui_log { + redis( + host("redis-mailcow") + persist-name("redis1") + port(6379) + auth("`REDISPASS`") + command("LPUSH" "SOGO_LOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n") + ); +}; +destination d_redis_f2b_channel { + redis( + host("redis-mailcow") + persist-name("redis2") + port(6379) + auth("`REDISPASS`") + command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)") + ); +}; +log { + source(s_sogo); + destination(d_redis_ui_log); + destination(d_redis_f2b_channel); +}; +log { + source(s_sogo); + source(s_src); + destination(d_stdout); +}; diff --git a/mailcow/data/Dockerfiles/unbound/Dockerfile b/mailcow/data/Dockerfiles/unbound/Dockerfile new file mode 100644 index 0000000..4903750 --- /dev/null +++ b/mailcow/data/Dockerfiles/unbound/Dockerfile @@ -0,0 +1,36 @@ +FROM alpine:3.21 + +LABEL maintainer = "The Infrastructure Company GmbH " + +RUN apk add --update --no-cache \ + curl \ + bind-tools \ + coreutils \ + unbound \ + bash \ + openssl \ + drill \ + tzdata \ + syslog-ng \ + supervisor \ + && curl -o /etc/unbound/root.hints https://www.internic.net/domain/named.cache \ + && chown root:unbound /etc/unbound \ + && adduser unbound tty \ + && chmod 775 /etc/unbound + +EXPOSE 53/udp 53/tcp + +COPY docker-entrypoint.sh /docker-entrypoint.sh + +# healthcheck (dig, ping) +COPY healthcheck.sh /healthcheck.sh +COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf +COPY supervisord.conf /etc/supervisor/supervisord.conf +COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh + +RUN chmod +x /healthcheck.sh +HEALTHCHECK --interval=30s --timeout=10s \ + CMD sh -c '[ -f /tmp/healthcheck_status ] && [ "$(cat /tmp/healthcheck_status)" -eq 0 ] || exit 1' + +ENTRYPOINT ["/docker-entrypoint.sh"] +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] diff --git a/mailcow/data/Dockerfiles/unbound/docker-entrypoint.sh b/mailcow/data/Dockerfiles/unbound/docker-entrypoint.sh new file mode 100755 index 0000000..bb9c115 --- /dev/null +++ b/mailcow/data/Dockerfiles/unbound/docker-entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +echo "Setting console permissions..." +chown root:tty /dev/console +chmod g+rw /dev/console +echo "Receiving anchor key..." +/usr/sbin/unbound-anchor -a /etc/unbound/trusted-key.key +echo "Receiving root hints..." +curl -#o /etc/unbound/root.hints https://www.internic.net/domain/named.cache +/usr/sbin/unbound-control-setup + +# Run hooks +for file in /hooks/*; do + if [ -x "${file}" ]; then + echo "Running hook ${file}" + "${file}" + fi +done + +exec "$@" diff --git a/mailcow/data/Dockerfiles/unbound/healthcheck.sh b/mailcow/data/Dockerfiles/unbound/healthcheck.sh new file mode 100644 index 0000000..7d91811 --- /dev/null +++ b/mailcow/data/Dockerfiles/unbound/healthcheck.sh @@ -0,0 +1,102 @@ +#!/bin/bash + +STATUS_FILE="/tmp/healthcheck_status" +RUNS=0 + +# Declare log function for logfile to stdout +function log_to_stdout() { +echo "$(date +"%Y-%m-%d %H:%M:%S"): $1" +} + +# General Ping function to check general pingability +function check_ping() { +declare -a ipstoping=("1.1.1.1" "8.8.8.8" "9.9.9.9") +local fail_tolerance=1 +local failures=0 + +for ip in "${ipstoping[@]}" ; do + success=false + for ((i=1; i<=3; i++)); do + ping -q -c 3 -w 5 "$ip" > /dev/null + if [ $? -eq 0 ]; then + success=true + break + else + log_to_stdout "Healthcheck: Failed to ping $ip on attempt $i. Trying again..." + fi + done + + if [ "$success" = false ]; then + log_to_stdout "Healthcheck: Couldn't ping $ip after 3 attempts. Marking this IP as failed." + ((failures++)) + fi +done + +if [ $failures -gt $fail_tolerance ]; then + log_to_stdout "Healthcheck: Too many ping failures ($fail_tolerance failures allowed, you got $failures failures), marking Healthcheck as unhealthy..." + return 1 +fi + +return 0 + +} + +# General DNS Resolve Check against Unbound Resolver himself +function check_dns() { +declare -a domains=("fuzzy.mailcow.email" "github.com" "hub.docker.com") +local fail_tolerance=1 +local failures=0 + +for domain in "${domains[@]}" ; do + success=false + for ((i=1; i<=3; i++)); do + dig_output=$(dig +short +timeout=2 +tries=1 "$domain" @127.0.0.1 2>/dev/null) + dig_rc=$? + + if [ $dig_rc -ne 0 ] || [ -z "$dig_output" ]; then + log_to_stdout "Healthcheck: DNS Resolution Failed on attempt $i for $domain! Trying again..." + else + success=true + break + fi + done + + if [ "$success" = false ]; then + log_to_stdout "Healthcheck: DNS Resolution not possible after 3 attempts for $domain... Gave up!" + ((failures++)) + fi +done + +if [ $failures -gt $fail_tolerance ]; then + log_to_stdout "Healthcheck: Too many DNS failures ($fail_tolerance failures allowed, you got $failures failures), marking Healthcheck as unhealthy..." + return 1 +fi + +return 0 +} + +while true; do + + if [[ ${SKIP_UNBOUND_HEALTHCHECK} == "y" ]]; then + log_to_stdout "Healthcheck: ALL CHECKS WERE SKIPPED! Unbound is healthy!" + echo "0" > $STATUS_FILE + sleep 365d + fi + + # run checks, if check is not returning 0 (return value if check is ok), healthcheck will exit with 1 (marked in docker as unhealthy) + check_ping + PING_STATUS=$? + + check_dns + DNS_STATUS=$? + + if [ $PING_STATUS -ne 0 ] || [ $DNS_STATUS -ne 0 ]; then + echo "1" > $STATUS_FILE + + else + echo "0" > $STATUS_FILE + fi + + sleep 30 + +done \ No newline at end of file diff --git a/mailcow/data/Dockerfiles/unbound/stop-supervisor.sh b/mailcow/data/Dockerfiles/unbound/stop-supervisor.sh new file mode 100755 index 0000000..acd4027 --- /dev/null +++ b/mailcow/data/Dockerfiles/unbound/stop-supervisor.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +printf "READY\n"; + +while read line; do + echo "Processing Event: $line" >&2; + kill -3 $(cat "/var/run/supervisord.pid") +done < /dev/stdin + +rm -rf /tmp/healthcheck_status \ No newline at end of file diff --git a/mailcow/data/Dockerfiles/unbound/supervisord.conf b/mailcow/data/Dockerfiles/unbound/supervisord.conf new file mode 100644 index 0000000..b47c8b1 --- /dev/null +++ b/mailcow/data/Dockerfiles/unbound/supervisord.conf @@ -0,0 +1,32 @@ +[supervisord] +nodaemon=true +user=root +pidfile=/var/run/supervisord.pid + +[program:syslog-ng] +command=/usr/sbin/syslog-ng --foreground --no-caps +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autostart=true + +[program:unbound] +command=/usr/sbin/unbound +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autorestart=true + +[program:unbound-healthcheck] +command=/bin/bash /healthcheck.sh +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autorestart=true + +[eventlistener:processes] +command=/usr/local/sbin/stop-supervisor.sh +events=PROCESS_STATE_STOPPED, PROCESS_STATE_EXITED, PROCESS_STATE_FATAL diff --git a/mailcow/data/Dockerfiles/unbound/syslog-ng.conf b/mailcow/data/Dockerfiles/unbound/syslog-ng.conf new file mode 100644 index 0000000..de858f9 --- /dev/null +++ b/mailcow/data/Dockerfiles/unbound/syslog-ng.conf @@ -0,0 +1,21 @@ +@version: 4.5 +@include "scl.conf" +options { + chain_hostnames(off); + flush_lines(0); + use_dns(no); + use_fqdn(no); + owner("root"); group("adm"); perm(0640); + stats(freq(0)); + keep_timestamp(no); + bad_hostname("^gconfd$"); +}; +source s_dgram { + unix-dgram("/dev/log"); + internal(); +}; +destination d_stdout { pipe("/dev/stdout"); }; +log { + source(s_dgram); + destination(d_stdout); +}; diff --git a/mailcow/data/Dockerfiles/watchdog/Dockerfile b/mailcow/data/Dockerfiles/watchdog/Dockerfile new file mode 100644 index 0000000..a55a97a --- /dev/null +++ b/mailcow/data/Dockerfiles/watchdog/Dockerfile @@ -0,0 +1,40 @@ +FROM alpine:3.21 + +LABEL maintainer = "The Infrastructure Company GmbH " + +# Installation +RUN apk add --update \ + && apk add --no-cache nagios-plugins-smtp \ + nagios-plugins-tcp \ + nagios-plugins-http \ + nagios-plugins-ping \ + mariadb-client \ + curl \ + bash \ + coreutils \ + jq \ + fcgi \ + openssl \ + nagios-plugins-mysql \ + nagios-plugins-dns \ + nagios-plugins-disk \ + bind-tools \ + redis \ + perl \ + perl-net-dns \ + perl-io-socket-ssl \ + perl-io-socket-inet6 \ + perl-socket \ + perl-socket6 \ + perl-mime-lite \ + perl-term-readkey \ + tini \ + tzdata \ + whois \ + && curl https://raw.githubusercontent.com/mludvig/smtp-cli/v3.10/smtp-cli -o /smtp-cli \ + && chmod +x smtp-cli + +COPY watchdog.sh /watchdog.sh +COPY check_mysql_slavestatus.sh /usr/lib/nagios/plugins/check_mysql_slavestatus.sh + +CMD ["/watchdog.sh"] diff --git a/mailcow/data/Dockerfiles/watchdog/check_mysql_slavestatus.sh b/mailcow/data/Dockerfiles/watchdog/check_mysql_slavestatus.sh new file mode 100755 index 0000000..42eec59 --- /dev/null +++ b/mailcow/data/Dockerfiles/watchdog/check_mysql_slavestatus.sh @@ -0,0 +1,223 @@ +#!/bin/bash +######################################################################### +# Script: check_mysql_slavestatus.sh # +# Author: Claudio Kuenzler www.claudiokuenzler.com # +# Purpose: Monitor MySQL Replication status with Nagios # +# Description: Connects to given MySQL hosts and checks for running # +# SLAVE state and delivers additional info # +# Original: This script is a modified version of # +# check mysql slave sql running written by dhirajt # +# Thanks to: Victor Balada Diaz for his ideas added on 20080930 # +# Soren Klintrup for stuff added on 20081015 # +# Marc Feret for Slave_IO_Running check 20111227 # +# Peter Lecki for his mods added on 20120803 # +# Serge Victor for his mods added on 20131223 # +# Omri Bahumi for his fix added on 20131230 # +# Marc Falzon for his option mods added on 20190822 # +# Andreas Pfeiffer for adding socket option on 20190822 # +# History: # +# 2008041700 Original Script modified # +# 2008041701 Added additional info if status OK # +# 2008041702 Added usage of script with params -H -u -p # +# 2008041703 Added bindir variable for multiple platforms # +# 2008041704 Added help because mankind needs help # +# 2008093000 Using /bin/sh instead of /bin/bash # +# 2008093001 Added port for MySQL server # +# 2008093002 Added mysqldir if mysql binary is elsewhere # +# 2008101501 Changed bindir/mysqldir to use PATH # +# 2008101501 Use $() instead of `` to avoid forks # +# 2008101501 Use ${} for variables to prevent problems # +# 2008101501 Check if required commands exist # +# 2008101501 Check if mysql connection works # +# 2008101501 Exit with unknown status at script end # +# 2008101501 Also display help if no option is given # +# 2008101501 Add warning/critical check to delay # +# 2011062200 Add perfdata # +# 2011122700 Checking Slave_IO_Running # +# 2012080300 Changed to use only one mysql query # +# 2012080301 Added warn and crit delay as optional args # +# 2012080302 Added standard -h option for syntax help # +# 2012080303 Added check for mandatory options passed in # +# 2012080304 Added error output from mysql # +# 2012080305 Changed from 'cut' to 'awk' (eliminate ws) # +# 2012111600 Do not show password in error output # +# 2013042800 Changed PATH to use existing PATH, too # +# 2013050800 Bugfix in PATH export # +# 2013092700 Bugfix in PATH export # +# 2013092701 Bugfix in getopts # +# 2013101600 Rewrite of threshold logic and handling # +# 2013101601 Optical clean up # +# 2013101602 Rewrite help output # +# 2013101700 Handle Slave IO in 'Connecting' state # +# 2013101701 Minor changes in output, handling UNKNOWN situations now # +# 2013101702 Exit CRITICAL when Slave IO in Connecting state # +# 2013123000 Slave_SQL_Running also matched Slave_SQL_Running_State # +# 2015011600 Added 'moving' check to catch possible connection issues # +# 2015011900 Use its own threshold for replication moving check # +# 2019082200 Add support for mysql option file # +# 2019082201 Improve password security (remove from mysql cli) # +# 2019082202 Added socket parameter (-S) # +# 2019082203 Use default port 3306, makes -P optional # +# 2019082204 Fix moving subcheck, improve documentation # +######################################################################### +# Usage: ./check_mysql_slavestatus.sh (-o file|(-H dbhost [-P port]|-S socket) -u dbuser -p dbpass) [-s connection] [-w integer] [-c integer] [-m integer] +######################################################################### +help="\ncheck_mysql_slavestatus.sh (c) 2008-2019 GNU GPLv2 licence +Usage: $0 (-o file|(-H dbhost [-P port]|-S socket) -u username -p password) [-s connection] [-w integer] [-c integer] [-m]\n +Options:\n-o Path to option file containing connection settings (e.g. /home/nagios/.my.cnf). Note: If this option is used, -H, -u, -p parameters will become optional\n-H Hostname or IP of slave server\n-P MySQL Port of slave server (optional, defaults to 3306)\n-u Username of DB-user\n-p Password of DB-user\n-S database socket\n-s Connection name (optional, with multi-source replication)\n-w Replication delay in seconds for Warning status (optional)\n-c Replication delay in seconds for Critical status (optional)\n-m Threshold in seconds since when replication did not move (compares the slaves log position)\n +Attention: The DB-user you type in must have CLIENT REPLICATION rights on the DB-server. Example:\n\tGRANT REPLICATION CLIENT on *.* TO 'nagios'@'%' IDENTIFIED BY 'secret';" + +STATE_OK=0 # define the exit code if status is OK +STATE_WARNING=1 # define the exit code if status is Warning (not really used) +STATE_CRITICAL=2 # define the exit code if status is Critical +STATE_UNKNOWN=3 # define the exit code if status is Unknown +export PATH=$PATH:/usr/local/bin:/usr/bin:/bin # Set path +crit="No" # what is the answer of MySQL Slave_SQL_Running for a Critical status? +ok="Yes" # what is the answer of MySQL Slave_SQL_Running for an OK status? +port="-P 3306" # on which tcp port is the target MySQL slave listening? + +for cmd in mysql awk grep expr [ +do + if ! `which ${cmd} &>/dev/null` + then + echo "UNKNOWN: This script requires the command '${cmd}' but it does not exist; please check if command exists and PATH is correct" + exit ${STATE_UNKNOWN} + fi +done + +# Check for people who need help +######################################################################### +if [ "${1}" = "--help" -o "${#}" = "0" ]; + then + echo -e "${help}"; + exit 1; +fi + +# Important given variables for the DB-Connect +######################################################################### +while getopts "H:P:u:p:S:s:w:c:o:m:h" Input; +do + case ${Input} in + H) host="-h ${OPTARG}";slavetarget=${OPTARG};; + P) port="-P ${OPTARG}";; + u) user="-u ${OPTARG}";; + p) password="${OPTARG}"; export MYSQL_PWD="${OPTARG}";; + S) socket="-S ${OPTARG}";; + s) connection=\"${OPTARG}\";; + w) warn_delay=${OPTARG};; + c) crit_delay=${OPTARG};; + o) optfile="--defaults-extra-file=${OPTARG}";; + m) moving=${OPTARG};; + h) echo -e "${help}"; exit 1;; + \?) echo "Wrong option given. Check help (-h, --help) for usage." + exit 1 + ;; + esac +done + +# Check if we can write to tmp +######################################################################### +test -w /tmp && tmpfile="/tmp/mysql_slave_${slavetarget}_pos.txt" + +# Connect to the DB server and check for informations +######################################################################### +# Check whether all required arguments were passed in (either option file or full connection settings) +if [[ -z "${optfile}" && -z "${host}" && -z "${socket}" ]]; then + echo -e "Missing required parameter(s)"; exit ${STATE_UNKNOWN} +elif [[ -n "${host}" && (-z "${user}" || -z "${password}") ]]; then + echo -e "Missing required parameter(s)"; exit ${STATE_UNKNOWN} +elif [[ -n "${socket}" && (-z "${user}" || -z "${password}") ]]; then + echo -e "Missing required parameter(s)"; exit ${STATE_UNKNOWN} +fi + +# Connect to the DB server and store output in vars +if [[ -n $socket ]]; then + ConnectionResult=$(mariadb --skip-ssl ${optfile} ${socket} ${user} -e "show slave ${connection} status\G" 2>&1) +else + ConnectionResult=$(mariadb --skip-ssl ${optfile} ${host} ${port} ${user} -e "show slave ${connection} status\G" 2>&1) +fi + +if [ -z "`echo "${ConnectionResult}" |grep Slave_IO_State`" ]; then + echo -e "CRITICAL: Unable to connect to server" + exit ${STATE_CRITICAL} +fi +check=`echo "${ConnectionResult}" |grep Slave_SQL_Running: | awk '{print $2}'` +checkio=`echo "${ConnectionResult}" |grep Slave_IO_Running: | awk '{print $2}'` +masterinfo=`echo "${ConnectionResult}" |grep Master_Host: | awk '{print $2}'` +delayinfo=`echo "${ConnectionResult}" |grep Seconds_Behind_Master: | awk '{print $2}'` +readpos=`echo "${ConnectionResult}" |grep Read_Master_Log_Pos: | awk '{print $2}'` +execpos=`echo "${ConnectionResult}" |grep Exec_Master_Log_Pos: | awk '{print $2}'` + +# Output of different exit states +######################################################################### +if [ ${check} = "NULL" ]; then +echo "CRITICAL: Slave_SQL_Running is answering NULL"; exit ${STATE_CRITICAL}; +fi + +if [ ${check} = ${crit} ]; then +echo "CRITICAL: ${host}:${port} Slave_SQL_Running: ${check}"; exit ${STATE_CRITICAL}; +fi + +if [ ${checkio} = ${crit} ]; then +echo "CRITICAL: ${host} Slave_IO_Running: ${checkio}"; exit ${STATE_CRITICAL}; +fi + +if [ ${checkio} = "Connecting" ]; then +echo "CRITICAL: ${host} Slave_IO_Running: ${checkio}"; exit ${STATE_CRITICAL}; +fi + +if [ ${check} = ${ok} ] && [ ${checkio} = ${ok} ]; then + # Delay thresholds are set + if [[ -n ${warn_delay} ]] && [[ -n ${crit_delay} ]]; then + if ! [[ ${warn_delay} -gt 0 ]]; then echo "Warning threshold must be a valid integer greater than 0"; exit $STATE_UNKNOWN; fi + if ! [[ ${crit_delay} -gt 0 ]]; then echo "Warning threshold must be a valid integer greater than 0"; exit $STATE_UNKNOWN; fi + if [[ -z ${warn_delay} ]] || [[ -z ${crit_delay} ]]; then echo "Both warning and critical thresholds must be set"; exit $STATE_UNKNOWN; fi + if [[ ${warn_delay} -gt ${crit_delay} ]]; then echo "Warning threshold cannot be greater than critical"; exit $STATE_UNKNOWN; fi + + if [[ ${delayinfo} -ge ${crit_delay} ]] + then echo "CRITICAL: Slave is ${delayinfo} seconds behind Master | delay=${delayinfo}s"; exit ${STATE_CRITICAL} + elif [[ ${delayinfo} -ge ${warn_delay} ]] + then echo "WARNING: Slave is ${delayinfo} seconds behind Master | delay=${delayinfo}s"; exit ${STATE_WARNING} + else + # Everything looks OK here but now let us check if the replication is moving + if [[ -n ${moving} ]] && [[ -n ${tmpfile} ]] && [[ $readpos -eq $execpos ]] + then + #echo "Debug: Read pos is $readpos - Exec pos is $execpos" + # Check if tmp file exists + curtime=`date +%s` + if [[ -w $tmpfile ]] + then + tmpfiletime=`date +%s -r $tmpfile` + if [[ `expr $curtime - $tmpfiletime` -gt ${moving} ]] + then + exectmp=`cat $tmpfile` + #echo "Debug: Exec pos in tmpfile is $exectmp" + if [[ $exectmp -eq $execpos ]] + then + # The value read from the tmp file and from db are the same. Replication hasnt moved! + echo "WARNING: Slave replication has not moved in ${moving} seconds. Manual check required."; exit ${STATE_WARNING} + else + # Replication has moved since the tmp file was written. Delete tmp file and output OK. + rm $tmpfile + echo "OK: Slave SQL running: ${check} Slave IO running: ${checkio} / master: ${masterinfo} / slave is ${delayinfo} seconds behind master | delay=${delayinfo}s"; exit ${STATE_OK}; + fi + else + echo "OK: Slave SQL running: ${check} Slave IO running: ${checkio} / master: ${masterinfo} / slave is ${delayinfo} seconds behind master | delay=${delayinfo}s"; exit ${STATE_OK}; + fi + else + echo "$execpos" > $tmpfile + echo "OK: Slave SQL running: ${check} Slave IO running: ${checkio} / master: ${masterinfo} / slave is ${delayinfo} seconds behind master | delay=${delayinfo}s"; exit ${STATE_OK}; + fi + else # Everything OK (no additional moving check) + echo "OK: Slave SQL running: ${check} Slave IO running: ${checkio} / master: ${masterinfo} / slave is ${delayinfo} seconds behind master | delay=${delayinfo}s"; exit ${STATE_OK}; + fi + fi + else + # Without delay thresholds + echo "OK: Slave SQL running: ${check} Slave IO running: ${checkio} / master: ${masterinfo} / slave is ${delayinfo} seconds behind master | delay=${delayinfo}s" + exit ${STATE_OK}; + fi +fi + +echo "UNKNOWN: should never reach this part (Slave_SQL_Running is ${check}, Slave_IO_Running is ${checkio})" +exit ${STATE_UNKNOWN} diff --git a/mailcow/data/Dockerfiles/watchdog/watchdog.sh b/mailcow/data/Dockerfiles/watchdog/watchdog.sh new file mode 100755 index 0000000..9d6f660 --- /dev/null +++ b/mailcow/data/Dockerfiles/watchdog/watchdog.sh @@ -0,0 +1,1126 @@ +#!/bin/bash + +trap "exit" INT TERM +trap "kill 0" EXIT + +# Prepare +BACKGROUND_TASKS=() +echo "Waiting for containers to settle..." +for i in {30..1}; do + echo "${i}" + sleep 1 +done + +if [[ "${USE_WATCHDOG}" =~ ^([nN][oO]|[nN])+$ ]]; then + echo -e "$(date) - USE_WATCHDOG=n, skipping watchdog..." + sleep 365d + exec $(readlink -f "$0") +fi + +if [[ "${WATCHDOG_VERBOSE}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + SMTP_VERBOSE="--verbose" + CURL_VERBOSE="--verbose" + set -xv +else + SMTP_VERBOSE="" + CURL_VERBOSE="" + exec 2>/dev/null +fi + +# Checks pipe their corresponding container name in this pipe +if [[ ! -p /tmp/com_pipe ]]; then + mkfifo /tmp/com_pipe +fi + +# Wait for containers +while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do + echo "Waiting for SQL..." + sleep 2 +done + +# Do not attempt to write to slave +if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then + REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning" +else + REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning" +fi + +until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do + echo "Waiting for Redis..." + sleep 2 +done + +${REDIS_CMDLINE} DEL F2B_RES > /dev/null + +# Common functions +get_ipv6(){ + local IPV6= + local IPV6_SRCS= + local TRY= + IPV6_SRCS[0]="ip6.mailcow.email" + IPV6_SRCS[1]="ip6.nevondo.com" + until [[ ! -z ${IPV6} ]] || [[ ${TRY} -ge 10 ]]; do + IPV6=$(curl --connect-timeout 3 -m 10 -L6s ${IPV6_SRCS[$RANDOM % ${#IPV6_SRCS[@]} ]} | grep "^\([0-9a-fA-F]\{0,4\}:\)\{1,7\}[0-9a-fA-F]\{0,4\}$") + [[ ! -z ${TRY} ]] && sleep 1 + TRY=$((TRY+1)) + done + echo ${IPV6} +} + +array_diff() { + # https://stackoverflow.com/questions/2312762, Alex Offshore + eval local ARR1=\(\"\${$2[@]}\"\) + eval local ARR2=\(\"\${$3[@]}\"\) + local IFS=$'\n' + mapfile -t $1 < <(comm -23 <(echo "${ARR1[*]}" | sort) <(echo "${ARR2[*]}" | sort)) +} + +progress() { + SERVICE=${1} + TOTAL=${2} + CURRENT=${3} + DIFF=${4} + [[ -z ${DIFF} ]] && DIFF=0 + [[ -z ${TOTAL} || -z ${CURRENT} ]] && return + [[ ${CURRENT} -gt ${TOTAL} ]] && return + [[ ${CURRENT} -lt 0 ]] && CURRENT=0 + PERCENT=$(( 200 * ${CURRENT} / ${TOTAL} % 2 + 100 * ${CURRENT} / ${TOTAL} )) + ${REDIS_CMDLINE} LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"service\":\"${SERVICE}\",\"lvl\":\"${PERCENT}\",\"hpnow\":\"${CURRENT}\",\"hptotal\":\"${TOTAL}\",\"hpdiff\":\"${DIFF}\"}" > /dev/null + log_msg "${SERVICE} health level: ${PERCENT}% (${CURRENT}/${TOTAL}), health trend: ${DIFF}" no_redis + # Return 10 to indicate a dead service + [ ${CURRENT} -le 0 ] && return 10 +} + +log_msg() { + if [[ ${2} != "no_redis" ]]; then + ${REDIS_CMDLINE} LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${1}" | \ + tr '\r\n%&;$"_[]{}-' ' ')\"}" > /dev/null + fi + echo $(date) $(printf '%s\n' "${1}") +} + +function notify_error() { + # Check if one of the notification options is enabled + [[ -z ${WATCHDOG_NOTIFY_EMAIL} ]] && [[ -z ${WATCHDOG_NOTIFY_WEBHOOK} ]] && return 0 + THROTTLE= + [[ -z ${1} ]] && return 1 + # If exists, body will be the content of "/tmp/${1}", even if ${2} is set + [[ -z ${2} ]] && BODY="Service was restarted on $(date), please check your mailcow installation." || BODY="$(date) - ${2}" + # If exists, mail will be throttled by argument in seconds + [[ ! -z ${3} ]] && THROTTLE=${3} + if [[ ! -z ${THROTTLE} ]]; then + TTL_LEFT="$(${REDIS_CMDLINE} TTL THROTTLE_${1} 2> /dev/null)" + if [[ "${TTL_LEFT}" == "-2" ]]; then + # Delay key not found, setting a delay key now + ${REDIS_CMDLINE} SET THROTTLE_${1} 1 EX ${THROTTLE} + else + log_msg "Not sending notification email now, blocked for ${TTL_LEFT} seconds..." + return 1 + fi + fi + WATCHDOG_NOTIFY_EMAIL=$(echo "${WATCHDOG_NOTIFY_EMAIL}" | sed 's/"//;s|"$||') + # Some exceptions for subject and body formats + if [[ ${1} == "fail2ban" ]]; then + SUBJECT="${BODY}" + BODY="Please see netfilter-mailcow for more details and triggered rules." + else + SUBJECT="${WATCHDOG_SUBJECT}: ${1}" + fi + + # Send mail notification if enabled + if [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]]; then + IFS=',' read -r -a MAIL_RCPTS <<< "${WATCHDOG_NOTIFY_EMAIL}" + for rcpt in "${MAIL_RCPTS[@]}"; do + RCPT_DOMAIN= + RCPT_MX= + RCPT_DOMAIN=$(echo ${rcpt} | awk -F @ {'print $NF'}) + CHECK_FOR_VALID_MX=$(dig +short ${RCPT_DOMAIN} mx) + if [[ -z ${CHECK_FOR_VALID_MX} ]]; then + log_msg "Cannot determine MX for ${rcpt}, skipping email notification..." + return 1 + fi + [ -f "/tmp/${1}" ] && BODY="/tmp/${1}" + timeout 10s ./smtp-cli --missing-modules-ok \ + "${SMTP_VERBOSE}" \ + --charset=UTF-8 \ + --subject="${SUBJECT}" \ + --body-plain="${BODY}" \ + --add-header="X-Priority: 1" \ + --to=${rcpt} \ + --from="watchdog@${MAILCOW_HOSTNAME}" \ + --hello-host=${MAILCOW_HOSTNAME} \ + --ipv4 + if [[ $? -eq 1 ]]; then # exit code 1 is fine + log_msg "Sent notification email to ${rcpt}" + else + if [[ "${SMTP_VERBOSE}" == "" ]]; then + log_msg "Error while sending notification email to ${rcpt}. You can enable verbose logging by setting 'WATCHDOG_VERBOSE=y' in mailcow.conf." + else + log_msg "Error while sending notification email to ${rcpt}." + fi + fi + done + fi + + # Send webhook notification if enabled + if [[ ! -z ${WATCHDOG_NOTIFY_WEBHOOK} ]]; then + if [[ -z ${WATCHDOG_NOTIFY_WEBHOOK_BODY} ]]; then + log_msg "No webhook body set, skipping webhook notification..." + return 1 + fi + + # Escape subject and body (https://stackoverflow.com/a/2705678) + ESCAPED_SUBJECT=$(echo ${SUBJECT} | sed -e 's/[\/&]/\\&/g') + ESCAPED_BODY=$(echo ${BODY} | sed -e 's/[\/&]/\\&/g') + + # Replace subject and body placeholders + WEBHOOK_BODY=$(echo ${WATCHDOG_NOTIFY_WEBHOOK_BODY} | sed -e "s/\$SUBJECT\|\${SUBJECT}/$ESCAPED_SUBJECT/g" -e "s/\$BODY\|\${BODY}/$ESCAPED_BODY/g") + + # POST to webhook + curl -X POST -H "Content-Type: application/json" ${CURL_VERBOSE} -d "${WEBHOOK_BODY}" ${WATCHDOG_NOTIFY_WEBHOOK} + + log_msg "Sent notification using webhook" + fi +} + +get_container_ip() { + # ${1} is container + CONTAINER_ID=() + CONTAINER_IPS=() + CONTAINER_IP= + LOOP_C=1 + until [[ ${CONTAINER_IP} =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] || [[ ${LOOP_C} -gt 5 ]]; do + if [ ${IP_BY_DOCKER_API} -eq 0 ]; then + CONTAINER_IP=$(dig a "${1}" +short) + else + sleep 0.5 + # get long container id for exact match + CONTAINER_ID=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring == \"${1}\") | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id")) + # returned id can have multiple elements (if scaled), shuffle for random test + CONTAINER_ID=($(printf "%s\n" "${CONTAINER_ID[@]}" | shuf)) + if [[ ! -z ${CONTAINER_ID} ]]; then + for matched_container in "${CONTAINER_ID[@]}"; do + CONTAINER_IPS=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${matched_container}/json | jq -r '.NetworkSettings.Networks[].IPAddress')) + for ip_match in "${CONTAINER_IPS[@]}"; do + # grep will do nothing if one of these vars is empty + [[ -z ${ip_match} ]] && continue + [[ -z ${IPV4_NETWORK} ]] && continue + # only return ips that are part of our network + if ! grep -q ${IPV4_NETWORK} <(echo ${ip_match}); then + continue + else + CONTAINER_IP=${ip_match} + break + fi + done + [[ ! -z ${CONTAINER_IP} ]] && break + done + fi + fi + LOOP_C=$((LOOP_C + 1)) + done + [[ ${LOOP_C} -gt 5 ]] && echo 240.0.0.0 || echo ${CONTAINER_IP} +} + +# One-time check +if grep -qi "$(echo ${IPV6_NETWORK} | cut -d: -f1-3)" <<< "$(ip a s)"; then + if [[ -z "$(get_ipv6)" ]]; then + notify_error "ipv6-config" "enable_ipv6 is true in docker-compose.yml, but an IPv6 link could not be established. Please verify your IPv6 connection." + fi +fi + +external_checks() { + err_count=0 + diff_c=0 + THRESHOLD=${EXTERNAL_CHECKS_THRESHOLD} + # Reduce error count by 2 after restarting an unhealthy container + GUID=$(mariadb --skip-ssl -u${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'GUID'" -BN) + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + err_c_cur=${err_count} + CHECK_REPONSE="$(curl --connect-timeout 3 -m 10 -4 -s https://checks.mailcow.email -X POST -dguid=${GUID} 2> /dev/null)" + if [[ ! -z "${CHECK_REPONSE}" ]] && [[ "$(echo ${CHECK_REPONSE} | jq -r .response)" == "critical" ]]; then + echo ${CHECK_REPONSE} | jq -r .out > /tmp/external_checks + err_count=$(( ${err_count} + 1 )) + fi + CHECK_REPONSE6="$(curl --connect-timeout 3 -m 10 -6 -s https://checks.mailcow.email -X POST -dguid=${GUID} 2> /dev/null)" + if [[ ! -z "${CHECK_REPONSE6}" ]] && [[ "$(echo ${CHECK_REPONSE6} | jq -r .response)" == "critical" ]]; then + echo ${CHECK_REPONSE} | jq -r .out > /tmp/external_checks + err_count=$(( ${err_count} + 1 )) + fi + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "External checks" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + if [[ $? == 10 ]]; then + diff_c=0 + sleep 60 + else + diff_c=0 + sleep $(( ( RANDOM % 20 ) + 1800 )) + fi + done + return 1 +} + +nginx_checks() { + err_count=0 + diff_c=0 + THRESHOLD=${NGINX_THRESHOLD} + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + touch /tmp/nginx-mailcow; echo "$(tail -50 /tmp/nginx-mailcow)" > /tmp/nginx-mailcow + host_ip=$(get_container_ip nginx-mailcow) + err_c_cur=${err_count} + /usr/lib/nagios/plugins/check_http -4 -H ${host_ip} -u / -p 8081 2>> /tmp/nginx-mailcow 1>&2; err_count=$(( ${err_count} + $? )) + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "Nginx" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + if [[ $? == 10 ]]; then + diff_c=0 + sleep 1 + else + diff_c=0 + sleep $(( ( RANDOM % 60 ) + 20 )) + fi + done + return 1 +} + +unbound_checks() { + err_count=0 + diff_c=0 + THRESHOLD=${UNBOUND_THRESHOLD} + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + touch /tmp/unbound-mailcow; echo "$(tail -50 /tmp/unbound-mailcow)" > /tmp/unbound-mailcow + host_ip=$(get_container_ip unbound-mailcow) + err_c_cur=${err_count} + /usr/lib/nagios/plugins/check_dns -s ${host_ip} -H stackoverflow.com 2>> /tmp/unbound-mailcow 1>&2; err_count=$(( ${err_count} + $? )) + DNSSEC=$(dig com +dnssec | egrep 'flags:.+ad') + if [[ -z ${DNSSEC} ]]; then + echo "DNSSEC failure" 2>> /tmp/unbound-mailcow 1>&2 + err_count=$(( ${err_count} + 1)) + else + echo "DNSSEC check succeeded" 2>> /tmp/unbound-mailcow 1>&2 + fi + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "Unbound" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + if [[ $? == 10 ]]; then + diff_c=0 + sleep 1 + else + diff_c=0 + sleep $(( ( RANDOM % 60 ) + 20 )) + fi + done + return 1 +} + +redis_checks() { + # A check for the local redis container + err_count=0 + diff_c=0 + THRESHOLD=${REDIS_THRESHOLD} + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + touch /tmp/redis-mailcow; echo "$(tail -50 /tmp/redis-mailcow)" > /tmp/redis-mailcow + host_ip=$(get_container_ip redis-mailcow) + err_c_cur=${err_count} + /usr/lib/nagios/plugins/check_tcp -4 -H redis-mailcow -p 6379 -E -s "AUTH ${REDISPASS}\nPING\n" -q "QUIT" -e "PONG" 2>> /tmp/redis-mailcow 1>&2; err_count=$(( ${err_count} + $? )) + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "Redis" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + if [[ $? == 10 ]]; then + diff_c=0 + sleep 1 + else + diff_c=0 + sleep $(( ( RANDOM % 60 ) + 20 )) + fi + done + return 1 +} + +mysql_checks() { + err_count=0 + diff_c=0 + THRESHOLD=${MYSQL_THRESHOLD} + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + touch /tmp/mysql-mailcow; echo "$(tail -50 /tmp/mysql-mailcow)" > /tmp/mysql-mailcow + err_c_cur=${err_count} + /usr/lib/nagios/plugins/check_mysql -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? )) + /usr/lib/nagios/plugins/check_mysql_query -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} -q "SELECT COUNT(*) FROM information_schema.tables" 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? )) + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "MySQL/MariaDB" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + if [[ $? == 10 ]]; then + diff_c=0 + sleep 1 + else + diff_c=0 + sleep $(( ( RANDOM % 60 ) + 20 )) + fi + done + return 1 +} + +mysql_repl_checks() { + err_count=0 + diff_c=0 + THRESHOLD=${MYSQL_REPLICATION_THRESHOLD} + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + touch /tmp/mysql_repl_checks; echo "$(tail -50 /tmp/mysql_repl_checks)" > /tmp/mysql_repl_checks + err_c_cur=${err_count} + /usr/lib/nagios/plugins/check_mysql_slavestatus.sh -S /var/run/mysqld/mysqld.sock -u root -p ${DBROOT} 2>> /tmp/mysql_repl_checks 1>&2; err_count=$(( ${err_count} + $? )) + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "MySQL/MariaDB replication" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + if [[ $? == 10 ]]; then + diff_c=0 + sleep 60 + else + diff_c=0 + sleep $(( ( RANDOM % 60 ) + 20 )) + fi + done + return 1 +} + +sogo_checks() { + err_count=0 + diff_c=0 + THRESHOLD=${SOGO_THRESHOLD} + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + touch /tmp/sogo-mailcow; echo "$(tail -50 /tmp/sogo-mailcow)" > /tmp/sogo-mailcow + host_ip=$(get_container_ip sogo-mailcow) + err_c_cur=${err_count} + /usr/lib/nagios/plugins/check_http -4 -H ${host_ip} -u /SOGo.index/ -p 20000 2>> /tmp/sogo-mailcow 1>&2; err_count=$(( ${err_count} + $? )) + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "SOGo" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + if [[ $? == 10 ]]; then + diff_c=0 + sleep 1 + else + diff_c=0 + sleep $(( ( RANDOM % 60 ) + 20 )) + fi + done + return 1 +} + +postfix_checks() { + err_count=0 + diff_c=0 + THRESHOLD=${POSTFIX_THRESHOLD} + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + touch /tmp/postfix-mailcow; echo "$(tail -50 /tmp/postfix-mailcow)" > /tmp/postfix-mailcow + host_ip=$(get_container_ip postfix-mailcow) + err_c_cur=${err_count} + /usr/lib/nagios/plugins/check_smtp -4 -H ${host_ip} -p 589 -f "watchdog@invalid" -C "RCPT TO:watchdog@localhost" -C DATA -C . -R 250 2>> /tmp/postfix-mailcow 1>&2; err_count=$(( ${err_count} + $? )) + /usr/lib/nagios/plugins/check_smtp -4 -H ${host_ip} -p 589 -S 2>> /tmp/postfix-mailcow 1>&2; err_count=$(( ${err_count} + $? )) + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "Postfix" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + if [[ $? == 10 ]]; then + diff_c=0 + sleep 1 + else + diff_c=0 + sleep $(( ( RANDOM % 60 ) + 20 )) + fi + done + return 1 +} + +clamd_checks() { + err_count=0 + diff_c=0 + THRESHOLD=${CLAMD_THRESHOLD} + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + touch /tmp/clamd-mailcow; echo "$(tail -50 /tmp/clamd-mailcow)" > /tmp/clamd-mailcow + host_ip=$(get_container_ip clamd-mailcow) + err_c_cur=${err_count} + /usr/lib/nagios/plugins/check_clamd -4 -H ${host_ip} 2>> /tmp/clamd-mailcow 1>&2; err_count=$(( ${err_count} + $? )) + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "Clamd" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + if [[ $? == 10 ]]; then + diff_c=0 + sleep 1 + else + diff_c=0 + sleep $(( ( RANDOM % 120 ) + 20 )) + fi + done + return 1 +} + +dovecot_checks() { + err_count=0 + diff_c=0 + THRESHOLD=${DOVECOT_THRESHOLD} + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + touch /tmp/dovecot-mailcow; echo "$(tail -50 /tmp/dovecot-mailcow)" > /tmp/dovecot-mailcow + host_ip=$(get_container_ip dovecot-mailcow) + err_c_cur=${err_count} + /usr/lib/nagios/plugins/check_smtp -4 -H ${host_ip} -p 24 -f "watchdog@invalid" -C "RCPT TO:" -L -R "User doesn't exist" 2>> /tmp/dovecot-mailcow 1>&2; err_count=$(( ${err_count} + $? )) + /usr/lib/nagios/plugins/check_imap -4 -H ${host_ip} -p 993 -S -e "OK " 2>> /tmp/dovecot-mailcow 1>&2; err_count=$(( ${err_count} + $? )) + /usr/lib/nagios/plugins/check_imap -4 -H ${host_ip} -p 143 -e "OK " 2>> /tmp/dovecot-mailcow 1>&2; err_count=$(( ${err_count} + $? )) + /usr/lib/nagios/plugins/check_tcp -4 -H ${host_ip} -p 10001 -e "VERSION" 2>> /tmp/dovecot-mailcow 1>&2; err_count=$(( ${err_count} + $? )) + /usr/lib/nagios/plugins/check_tcp -4 -H ${host_ip} -p 4190 -e "Dovecot ready" 2>> /tmp/dovecot-mailcow 1>&2; err_count=$(( ${err_count} + $? )) + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "Dovecot" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + if [[ $? == 10 ]]; then + diff_c=0 + sleep 1 + else + diff_c=0 + sleep $(( ( RANDOM % 60 ) + 20 )) + fi + done + return 1 +} + +dovecot_repl_checks() { + err_count=0 + diff_c=0 + THRESHOLD=${DOVECOT_REPL_THRESHOLD} + D_REPL_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning -r GET DOVECOT_REPL_HEALTH) + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + err_c_cur=${err_count} + D_REPL_STATUS=$(redis-cli --raw -h redis -a ${REDISPASS} --no-auth-warning GET DOVECOT_REPL_HEALTH) + if [[ "${D_REPL_STATUS}" != "1" ]]; then + err_count=$(( ${err_count} + 1 )) + fi + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "Dovecot replication" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + if [[ $? == 10 ]]; then + diff_c=0 + sleep 60 + else + diff_c=0 + sleep $(( ( RANDOM % 60 ) + 20 )) + fi + done + return 1 +} + +cert_checks() { + err_count=0 + diff_c=0 + THRESHOLD=7 + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + touch /tmp/certcheck; echo "$(tail -50 /tmp/certcheck)" > /tmp/certcheck + host_ip_postfix=$(get_container_ip postfix) + host_ip_dovecot=$(get_container_ip dovecot) + err_c_cur=${err_count} + /usr/lib/nagios/plugins/check_smtp -H ${host_ip_postfix} -p 589 -4 -S -D 7 2>> /tmp/certcheck 1>&2; err_count=$(( ${err_count} + $? )) + /usr/lib/nagios/plugins/check_imap -H ${host_ip_dovecot} -p 993 -4 -S -D 7 2>> /tmp/certcheck 1>&2; err_count=$(( ${err_count} + $? )) + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "Primary certificate expiry check" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + # Always sleep 5 minutes, mail notifications are limited + sleep 300 + done + return 1 +} + +phpfpm_checks() { + err_count=0 + diff_c=0 + THRESHOLD=${PHPFPM_THRESHOLD} + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + touch /tmp/php-fpm-mailcow; echo "$(tail -50 /tmp/php-fpm-mailcow)" > /tmp/php-fpm-mailcow + host_ip=$(get_container_ip php-fpm-mailcow) + err_c_cur=${err_count} + /usr/lib/nagios/plugins/check_tcp -H ${host_ip} -p 9001 2>> /tmp/php-fpm-mailcow 1>&2; err_count=$(( ${err_count} + $? )) + /usr/lib/nagios/plugins/check_tcp -H ${host_ip} -p 9002 2>> /tmp/php-fpm-mailcow 1>&2; err_count=$(( ${err_count} + $? )) + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "PHP-FPM" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + if [[ $? == 10 ]]; then + diff_c=0 + sleep 1 + else + diff_c=0 + sleep $(( ( RANDOM % 60 ) + 20 )) + fi + done + return 1 +} + +ratelimit_checks() { + err_count=0 + diff_c=0 + THRESHOLD=${RATELIMIT_THRESHOLD} + RL_LOG_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning LRANGE RL_LOG 0 0 | jq .qid) + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + err_c_cur=${err_count} + RL_LOG_STATUS_PREV=${RL_LOG_STATUS} + RL_LOG_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning LRANGE RL_LOG 0 0 | jq .qid) + if [[ ${RL_LOG_STATUS_PREV} != ${RL_LOG_STATUS} ]]; then + err_count=$(( ${err_count} + 1 )) + echo 'Last 10 applied ratelimits (may overlap with previous reports).' > /tmp/ratelimit + echo 'Full ratelimit buckets can be emptied by deleting the ratelimit hash from within mailcow UI (see /debug -> Protocols -> Ratelimit):' >> /tmp/ratelimit + echo >> /tmp/ratelimit + redis-cli --raw -h redis -a ${REDISPASS} --no-auth-warning LRANGE RL_LOG 0 10 | jq . >> /tmp/ratelimit + fi + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "Ratelimit" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + if [[ $? == 10 ]]; then + diff_c=0 + sleep 1 + else + diff_c=0 + sleep $(( ( RANDOM % 60 ) + 20 )) + fi + done + return 1 +} + +mailq_checks() { + err_count=0 + diff_c=0 + THRESHOLD=${MAILQ_THRESHOLD} + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + touch /tmp/mail_queue_status; echo "$(tail -50 /tmp/mail_queue_status)" > /tmp/mail_queue_status + MAILQ_LOG_STATUS=$(find /var/spool/postfix/deferred -type f | wc -l) + echo "Mail queue contains ${MAILQ_LOG_STATUS} items (critical limit is ${MAILQ_CRIT}) at $(date)" >> /tmp/mail_queue_status + err_c_cur=${err_count} + if [ ${MAILQ_LOG_STATUS} -ge ${MAILQ_CRIT} ]; then + err_count=$(( ${err_count} + 1 )) + echo "Mail queue contains ${MAILQ_LOG_STATUS} items (critical limit is ${MAILQ_CRIT}) at $(date)" >> /tmp/mail_queue_status + fi + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "Mail queue" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + if [[ $? == 10 ]]; then + diff_c=0 + sleep 60 + else + diff_c=0 + sleep $(( ( RANDOM % 60 ) + 20 )) + fi + done + return 1 +} + +fail2ban_checks() { + err_count=0 + diff_c=0 + THRESHOLD=${FAIL2BAN_THRESHOLD} + F2B_LOG_STATUS=($(${REDIS_CMDLINE} --raw HKEYS F2B_ACTIVE_BANS)) + F2B_RES= + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + err_c_cur=${err_count} + F2B_LOG_STATUS_PREV=(${F2B_LOG_STATUS[@]}) + F2B_LOG_STATUS=($(${REDIS_CMDLINE} --raw HKEYS F2B_ACTIVE_BANS)) + array_diff F2B_RES F2B_LOG_STATUS F2B_LOG_STATUS_PREV + if [[ ! -z "${F2B_RES}" ]]; then + err_count=$(( ${err_count} + 1 )) + echo -n "${F2B_RES[@]}" | tr -cd "[a-fA-F0-9.:/] " | timeout 3s ${REDIS_CMDLINE} -x SET F2B_RES > /dev/null + if [ $? -ne 0 ]; then + ${REDIS_CMDLINE} -x DEL F2B_RES + fi + fi + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "Fail2ban" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + if [[ $? == 10 ]]; then + diff_c=0 + sleep 1 + else + diff_c=0 + sleep $(( ( RANDOM % 60 ) + 20 )) + fi + done + return 1 +} + +acme_checks() { + err_count=0 + diff_c=0 + THRESHOLD=${ACME_THRESHOLD} + ACME_LOG_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning GET ACME_FAIL_TIME) + if [[ -z "${ACME_LOG_STATUS}" ]]; then + ${REDIS_CMDLINE} SET ACME_FAIL_TIME 0 + ACME_LOG_STATUS=0 + fi + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + err_c_cur=${err_count} + ACME_LOG_STATUS_PREV=${ACME_LOG_STATUS} + ACME_LC=0 + until [[ ! -z ${ACME_LOG_STATUS} ]] || [ ${ACME_LC} -ge 3 ]; do + ACME_LOG_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning GET ACME_FAIL_TIME 2> /dev/null) + sleep 3 + ACME_LC=$((ACME_LC+1)) + done + if [[ ${ACME_LOG_STATUS_PREV} != ${ACME_LOG_STATUS} ]]; then + err_count=$(( ${err_count} + 1 )) + fi + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "ACME" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + if [[ $? == 10 ]]; then + diff_c=0 + sleep 1 + else + diff_c=0 + sleep $(( ( RANDOM % 60 ) + 20 )) + fi + done + return 1 +} + +rspamd_checks() { + err_count=0 + diff_c=0 + THRESHOLD=${RSPAMD_THRESHOLD} + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + touch /tmp/rspamd-mailcow; echo "$(tail -50 /tmp/rspamd-mailcow)" > /tmp/rspamd-mailcow + host_ip=$(get_container_ip rspamd-mailcow) + err_c_cur=${err_count} + SCORE=$(echo 'To: null@localhost +From: watchdog@localhost + +Empty +' | usr/bin/curl --max-time 10 -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/scan | jq -rc .default.required_score | sed 's/\..*//' ) + if [[ ${SCORE} -ne 9999 ]]; then + echo "Rspamd settings check failed, score returned: ${SCORE}" 2>> /tmp/rspamd-mailcow 1>&2 + err_count=$(( ${err_count} + 1)) + else + echo "Rspamd settings check succeeded, score returned: ${SCORE}" 2>> /tmp/rspamd-mailcow 1>&2 + fi + # A dirty hack until a PING PONG event is implemented to worker proxy + # We expect an empty response, not a timeout + if [ "$(curl -s --max-time 10 ${host_ip}:9900 2> /dev/null ; echo $?)" == "28" ]; then + echo "Milter check failed" 2>> /tmp/rspamd-mailcow 1>&2; err_count=$(( ${err_count} + 1 )); + else + echo "Milter check succeeded" 2>> /tmp/rspamd-mailcow 1>&2 + fi + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "Rspamd" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + if [[ $? == 10 ]]; then + diff_c=0 + sleep 1 + else + diff_c=0 + sleep $(( ( RANDOM % 60 ) + 20 )) + fi + done + return 1 +} + +olefy_checks() { + err_count=0 + diff_c=0 + THRESHOLD=${OLEFY_THRESHOLD} + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + touch /tmp/olefy-mailcow; echo "$(tail -50 /tmp/olefy-mailcow)" > /tmp/olefy-mailcow + host_ip=$(get_container_ip olefy-mailcow) + err_c_cur=${err_count} + /usr/lib/nagios/plugins/check_tcp -4 -H ${host_ip} -p 10055 -s "PING\n" 2>> /tmp/olefy-mailcow 1>&2; err_count=$(( ${err_count} + $? )) + [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 + [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) + progress "Olefy" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + if [[ $? == 10 ]]; then + diff_c=0 + sleep 1 + else + diff_c=0 + sleep $(( ( RANDOM % 60 ) + 20 )) + fi + done + return 1 +} + +# Notify about start +if [[ ${WATCHDOG_NOTIFY_START} =~ ^([yY][eE][sS]|[yY])+$ ]]; then + notify_error "watchdog-mailcow" "Watchdog started monitoring mailcow." +fi + +# Create watchdog agents + +( +while true; do + if ! nginx_checks; then + log_msg "Nginx hit error limit" + echo nginx-mailcow > /tmp/com_pipe + fi +done +) & +PID=$! +echo "Spawned nginx_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) + +if [[ ${WATCHDOG_EXTERNAL_CHECKS} =~ ^([yY][eE][sS]|[yY])+$ ]]; then +( +while true; do + if ! external_checks; then + log_msg "External checks hit error limit" + echo external_checks > /tmp/com_pipe + fi +done +) & +PID=$! +echo "Spawned external_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) +fi + +if [[ ${WATCHDOG_MYSQL_REPLICATION_CHECKS} =~ ^([yY][eE][sS]|[yY])+$ ]]; then +( +while true; do + if ! mysql_repl_checks; then + log_msg "MySQL replication check hit error limit" + echo mysql_repl_checks > /tmp/com_pipe + fi +done +) & +PID=$! +echo "Spawned mysql_repl_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) +fi + +( +while true; do + if ! mysql_checks; then + log_msg "MySQL hit error limit" + echo mysql-mailcow > /tmp/com_pipe + fi +done +) & +PID=$! +echo "Spawned mysql_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) + +( +while true; do + if ! redis_checks; then + log_msg "Local Redis hit error limit" + echo redis-mailcow > /tmp/com_pipe + fi +done +) & +PID=$! +echo "Spawned redis_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) + +( +while true; do + if ! phpfpm_checks; then + log_msg "PHP-FPM hit error limit" + echo php-fpm-mailcow > /tmp/com_pipe + fi +done +) & +PID=$! +echo "Spawned phpfpm_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) + +if [[ "${SKIP_SOGO}" =~ ^([nN][oO]|[nN])+$ ]]; then +( +while true; do + if ! sogo_checks; then + log_msg "SOGo hit error limit" + echo sogo-mailcow > /tmp/com_pipe + fi +done +) & +PID=$! +echo "Spawned sogo_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) +fi + +if [ ${CHECK_UNBOUND} -eq 1 ]; then +( +while true; do + if ! unbound_checks; then + log_msg "Unbound hit error limit" + echo unbound-mailcow > /tmp/com_pipe + fi +done +) & +PID=$! +echo "Spawned unbound_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) +fi + +if [[ "${SKIP_CLAMD}" =~ ^([nN][oO]|[nN])+$ ]]; then +( +while true; do + if ! clamd_checks; then + log_msg "Clamd hit error limit" + echo clamd-mailcow > /tmp/com_pipe + fi +done +) & +PID=$! +echo "Spawned clamd_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) +fi + +( +while true; do + if ! postfix_checks; then + log_msg "Postfix hit error limit" + echo postfix-mailcow > /tmp/com_pipe + fi +done +) & +PID=$! +echo "Spawned postfix_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) + +( +while true; do + if ! mailq_checks; then + log_msg "Mail queue hit error limit" + echo mail_queue_status > /tmp/com_pipe + fi +done +) & +PID=$! +echo "Spawned mailq_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) + +( +while true; do + if ! dovecot_checks; then + log_msg "Dovecot hit error limit" + echo dovecot-mailcow > /tmp/com_pipe + fi +done +) & +PID=$! +echo "Spawned dovecot_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) + +( +while true; do + if ! dovecot_repl_checks; then + log_msg "Dovecot hit error limit" + echo dovecot_repl_checks > /tmp/com_pipe + fi +done +) & +PID=$! +echo "Spawned dovecot_repl_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) + +( +while true; do + if ! rspamd_checks; then + log_msg "Rspamd hit error limit" + echo rspamd-mailcow > /tmp/com_pipe + fi +done +) & +PID=$! +echo "Spawned rspamd_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) + +( +while true; do + if ! ratelimit_checks; then + log_msg "Ratelimit hit error limit" + echo ratelimit > /tmp/com_pipe + fi +done +) & +PID=$! +echo "Spawned ratelimit_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) + +( +while true; do + if ! fail2ban_checks; then + log_msg "Fail2ban hit error limit" + echo fail2ban > /tmp/com_pipe + fi +done +) & +PID=$! +echo "Spawned fail2ban_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) + +( +while true; do + if ! cert_checks; then + log_msg "Cert check hit error limit" + echo certcheck > /tmp/com_pipe + fi +done +) & +PID=$! +echo "Spawned cert_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) + +( +while true; do + if ! olefy_checks; then + log_msg "Olefy hit error limit" + echo olefy-mailcow > /tmp/com_pipe + fi +done +) & +PID=$! +echo "Spawned olefy_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) + +( +while true; do + if ! acme_checks; then + log_msg "ACME client hit error limit" + echo acme-mailcow > /tmp/com_pipe + fi +done +) & +PID=$! +echo "Spawned acme_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) + +# Monitor watchdog agents, stop script when agents fails and wait for respawn by Docker (restart:always:n) +( +while true; do + for bg_task in ${BACKGROUND_TASKS[*]}; do + if ! kill -0 ${bg_task} 1>&2; then + log_msg "Worker ${bg_task} died, stopping watchdog and waiting for respawn..." + kill -TERM 1 + fi + sleep 10 + done +done +) & + +# Monitor dockerapi +( +while true; do + while nc -z dockerapi 443; do + sleep 3 + done + log_msg "Cannot find dockerapi-mailcow, waiting to recover..." + kill -STOP ${BACKGROUND_TASKS[*]} + until nc -z dockerapi 443; do + sleep 3 + done + kill -CONT ${BACKGROUND_TASKS[*]} + kill -USR1 ${BACKGROUND_TASKS[*]} +done +) & + +# Actions when threshold limit is reached +while true; do + CONTAINER_ID= + HAS_INITDB= + read com_pipe_answer /dev/null)) + if [[ ! -z "${F2B_RES}" ]]; then + ${REDIS_CMDLINE} DEL F2B_RES > /dev/null + host= + for host in "${F2B_RES[@]}"; do + log_msg "Banned ${host}" + rm /tmp/fail2ban 2> /dev/null + timeout 2s whois "${host}" > /tmp/fail2ban + [[ ${WATCHDOG_NOTIFY_BAN} =~ ^([yY][eE][sS]|[yY])+$ ]] && notify_error "${com_pipe_answer}" "IP ban: ${host}" + done + fi + elif [[ ${com_pipe_answer} =~ .+-mailcow ]]; then + kill -STOP ${BACKGROUND_TASKS[*]} + sleep 10 + CONTAINER_ID=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"${com_pipe_answer}\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id") + if [[ ! -z ${CONTAINER_ID} ]]; then + if [[ "${com_pipe_answer}" == "php-fpm-mailcow" ]]; then + HAS_INITDB=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/top | jq '.msg.Processes[] | contains(["php -c /usr/local/etc/php -f /web/inc/init_db.inc.php"])' | grep true) + fi + S_RUNNING=$(($(date +%s) - $(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/json | jq .State.StartedAt | xargs -n1 date +%s -d))) + if [ ${S_RUNNING} -lt 360 ]; then + log_msg "Container is running for less than 360 seconds, skipping action..." + elif [[ ! -z ${HAS_INITDB} ]]; then + log_msg "Database is being initialized by php-fpm-mailcow, not restarting but delaying checks for a minute..." + sleep 60 + else + log_msg "Sending restart command to ${CONTAINER_ID}..." + curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/restart + notify_error "${com_pipe_answer}" + log_msg "Wait for restarted container to settle and continue watching..." + sleep 35 + fi + fi + kill -CONT ${BACKGROUND_TASKS[*]} + sleep 1 + kill -USR1 ${BACKGROUND_TASKS[*]} + fi +done + diff --git a/mailcow/data/assets/mysql/docker-entrypoint.sh b/mailcow/data/assets/mysql/docker-entrypoint.sh new file mode 100755 index 0000000..94e394a --- /dev/null +++ b/mailcow/data/assets/mysql/docker-entrypoint.sh @@ -0,0 +1,192 @@ +#!/bin/bash +set -eo pipefail +shopt -s nullglob + +openssl req -x509 -sha256 -newkey rsa:2048 -keyout /var/lib/mysql/sql.key -out /var/lib/mysql/sql.crt -days 3650 -nodes -subj '/CN=mysql' + +# if command starts with an option, prepend mysqld +if [ "${1:0:1}" = '-' ]; then + set -- mysqld "$@" +fi + +# skip setup if they want an option that stops mysqld +wantHelp= +for arg; do + case "$arg" in + -'?'|--help|--print-defaults|-V|--version) + wantHelp=1 + break + ;; + esac +done + +# usage: file_env VAR [DEFAULT] +# ie: file_env 'XYZ_DB_PASSWORD' 'example' +# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of +# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature) +file_env() { + local var="$1" + local fileVar="${var}_FILE" + local def="${2:-}" + if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then + echo >&2 "error: both $var and $fileVar are set (but are exclusive)" + exit 1 + fi + local val="$def" + if [ "${!var:-}" ]; then + val="${!var}" + elif [ "${!fileVar:-}" ]; then + val="$(< "${!fileVar}")" + fi + export "$var"="$val" + unset "$fileVar" +} + +_check_config() { + toRun=( "$@" --verbose --help --log-bin-index="$(mktemp -u)" ) + if ! errors="$("${toRun[@]}" 2>&1 >/dev/null)"; then + cat >&2 <<-EOM + + ERROR: mysqld failed while attempting to check config + command was: "${toRun[*]}" + + $errors + EOM + exit 1 + fi +} + +# Fetch value from server config +# We use mysqld --verbose --help instead of my_print_defaults because the +# latter only show values present in config files, and not server defaults +_get_config() { + local conf="$1"; shift + "$@" --verbose --help --log-bin-index="$(mktemp -u)" 2>/dev/null | awk '$1 == "'"$conf"'" { print $2; exit }' +} + +# allow the container to be started with `--user` +if [ "$1" = 'mysqld' -a -z "$wantHelp" -a "$(id -u)" = '0' ]; then + _check_config "$@" + DATADIR="$(_get_config 'datadir' "$@")" + mkdir -p "$DATADIR" + chown -R mysql:mysql "$DATADIR" + exec gosu mysql "$BASH_SOURCE" "$@" +fi + +if [ "$1" = 'mysqld' -a -z "$wantHelp" ]; then + # still need to check config, container may have started with --user + _check_config "$@" + # Get config + DATADIR="$(_get_config 'datadir' "$@")" + + if [ ! -d "$DATADIR/mysql" ]; then + file_env 'MYSQL_ROOT_PASSWORD' + if [ -z "$MYSQL_ROOT_PASSWORD" -a -z "$MYSQL_ALLOW_EMPTY_PASSWORD" -a -z "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then + echo >&2 'error: database is uninitialized and password option is not specified ' + echo >&2 ' You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD' + exit 1 + fi + + mkdir -p "$DATADIR" + + echo 'Initializing database' + # "Other options are passed to mysqld." (so we pass all "mysqld" arguments directly here) + mysql_install_db --datadir="$DATADIR" --rpm "${@:2}" + echo 'Database initialized' + + SOCKET="$(_get_config 'socket' "$@")" + "$@" --skip-networking --socket="${SOCKET}" & + pid="$!" + + mysql=( mysql --protocol=socket -uroot -hlocalhost --socket="${SOCKET}" ) + + for i in {30..0}; do + if echo 'SELECT 1' | "${mysql[@]}" &> /dev/null; then + break + fi + echo 'MySQL init process in progress...' + sleep 1 + done + if [ "$i" = 0 ]; then + echo >&2 'MySQL init process failed.' + exit 1 + fi + + if [ -z "$MYSQL_INITDB_SKIP_TZINFO" ]; then + # sed is for https://bugs.mysql.com/bug.php?id=20545 + mysql_tzinfo_to_sql /usr/share/zoneinfo | sed 's/Local time zone must be set--see zic manual page/FCTY/' | "${mysql[@]}" mysql + fi + + if [ ! -z "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then + export MYSQL_ROOT_PASSWORD="$(pwgen -1 32)" + echo "GENERATED ROOT PASSWORD: $MYSQL_ROOT_PASSWORD" + fi + + rootCreate= + # default root to listen for connections from anywhere + file_env 'MYSQL_ROOT_HOST' '%' + if [ ! -z "$MYSQL_ROOT_HOST" -a "$MYSQL_ROOT_HOST" != 'localhost' ]; then + # no, we don't care if read finds a terminating character in this heredoc + # https://unix.stackexchange.com/questions/265149/why-is-set-o-errexit-breaking-this-read-heredoc-expression/265151#265151 + read -r -d '' rootCreate <<-EOSQL || true + CREATE USER 'root'@'${MYSQL_ROOT_HOST}' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ; + GRANT ALL ON *.* TO 'root'@'${MYSQL_ROOT_HOST}' WITH GRANT OPTION ; + EOSQL + fi + + "${mysql[@]}" <<-EOSQL + -- What's done in this file shouldn't be replicated + -- or products like mysql-fabric won't work + SET @@SESSION.SQL_LOG_BIN=0; + + DELETE FROM mysql.user WHERE user NOT IN ('mysql.sys', 'mysqlxsys', 'root') OR host NOT IN ('localhost') ; + SET PASSWORD FOR 'root'@'localhost'=PASSWORD('${MYSQL_ROOT_PASSWORD}') ; + GRANT ALL ON *.* TO 'root'@'localhost' WITH GRANT OPTION ; + ${rootCreate} + DROP DATABASE IF EXISTS test ; + FLUSH PRIVILEGES ; + EOSQL + + if [ ! -z "$MYSQL_ROOT_PASSWORD" ]; then + mysql+=( -p"${MYSQL_ROOT_PASSWORD}" ) + fi + + file_env 'MYSQL_DATABASE' + if [ "$MYSQL_DATABASE" ]; then + echo "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` ;" | "${mysql[@]}" + mysql+=( "$MYSQL_DATABASE" ) + fi + + file_env 'MYSQL_USER' + file_env 'MYSQL_PASSWORD' + if [ "$MYSQL_USER" -a "$MYSQL_PASSWORD" ]; then + echo "CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD' ;" | "${mysql[@]}" + + if [ "$MYSQL_DATABASE" ]; then + echo "GRANT ALL ON \`$MYSQL_DATABASE\`.* TO '$MYSQL_USER'@'%' ;" | "${mysql[@]}" + fi + fi + + echo + for f in /docker-entrypoint-initdb.d/*; do + case "$f" in + *.sh) echo "$0: running $f"; . "$f" ;; + *.sql) echo "$0: running $f"; "${mysql[@]}" < "$f"; echo ;; + *.sql.gz) echo "$0: running $f"; gunzip -c "$f" | "${mysql[@]}"; echo ;; + *) echo "$0: ignoring $f" ;; + esac + echo + done + + if ! kill -s TERM "$pid" || ! wait "$pid"; then + echo >&2 'MySQL init process failed.' + exit 1 + fi + + echo + echo 'MySQL init process done. Ready for start up.' + echo + fi +fi + +exec "$@" diff --git a/mailcow/data/assets/passwd/generate_passwords.sh b/mailcow/data/assets/passwd/generate_passwords.sh new file mode 100755 index 0000000..7861315 --- /dev/null +++ b/mailcow/data/assets/passwd/generate_passwords.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +echo DBPASS=$(openssl rand -base64 32 | tr -dc _A-Z-a-z-0-9) +echo DBROOT=$(openssl rand -base64 32 | tr -dc _A-Z-a-z-0-9) diff --git a/mailcow/data/assets/templates/pw_reset_html.tpl b/mailcow/data/assets/templates/pw_reset_html.tpl new file mode 100644 index 0000000..481f8cb --- /dev/null +++ b/mailcow/data/assets/templates/pw_reset_html.tpl @@ -0,0 +1,29 @@ + + + + + + +Hello {{username2}},

+ +Somebody requested a new password for the {{hostname}} account associated with {{username}}.
+Date of the password reset request: {{date}}

+ +You can reset your password by clicking the link below:
+
{{link}}

+ +The link will be valid for the next {{token_lifetime}} minutes.

+ +If you did not request a new password, please ignore this email.
+ + diff --git a/mailcow/data/assets/templates/pw_reset_text.tpl b/mailcow/data/assets/templates/pw_reset_text.tpl new file mode 100644 index 0000000..fabe1e7 --- /dev/null +++ b/mailcow/data/assets/templates/pw_reset_text.tpl @@ -0,0 +1,11 @@ +Hello {{username2}}, + +Somebody requested a new password for the {{hostname}} account associated with {{username}}. +Date of the password reset request: {{date}} + +You can reset your password by clicking the link below: +{{link}} + +The link will be valid for the next {{token_lifetime}} minutes. + +If you did not request a new password, please ignore this email. diff --git a/mailcow/data/assets/templates/quarantine.tpl b/mailcow/data/assets/templates/quarantine.tpl new file mode 100644 index 0000000..8fa88c5 --- /dev/null +++ b/mailcow/data/assets/templates/quarantine.tpl @@ -0,0 +1,69 @@ + + + + + + +

Hi {{username}}!
+ {% if counter == 1 %} + There is 1 new message waiting in quarantine:
+ {% else %} + There are {{counter}} new messages waiting in quarantine:
+ {% endif %} +

+ {% if quarantine_acl == 1 %}{% endif %} + {% for line in meta|reverse %} + + + + + {% if line.action == "reject" %} + + {% else %} + + {% endif %} + + {% if quarantine_acl == 1 %} + {% if line.action == "reject" %} + + {% else %} + + {% endif %} + {% endif %} + + {% endfor %} +
SubjectSenderScoreActionArrived onActions
{{ line.subject|e }}{{ line.sender|e }}{{ line.score }}RejectedSent to Junk folder{{ line.created }}Release to inbox | deleteSend copy to inbox | delete
+

+ + diff --git a/mailcow/data/assets/templates/quota.tpl b/mailcow/data/assets/templates/quota.tpl new file mode 100644 index 0000000..b6ad644 --- /dev/null +++ b/mailcow/data/assets/templates/quota.tpl @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + +
+ Hi {{username}}!

+ Your mailbox is now {{percent}}% full, please consider deleting old messages to still be able to receive new mails in the future.

+
{{percent}}%
+ + diff --git a/mailcow/data/conf/clamav/clamd.conf b/mailcow/data/conf/clamav/clamd.conf new file mode 100644 index 0000000..df1aa1e --- /dev/null +++ b/mailcow/data/conf/clamav/clamd.conf @@ -0,0 +1,47 @@ +#Debug true +#LogFile /dev/null +LogTime yes +LogClean yes +ExtendedDetectionInfo yes +PidFile /run/clamav/clamd.pid +OfficialDatabaseOnly no +LocalSocket /run/clamav/clamd.sock +TCPSocket 3310 +StreamMaxLength 25M +MaxThreads 10 +ReadTimeout 10 +CommandReadTimeout 3 +SendBufTimeout 200 +MaxQueue 80 +IdleTimeout 20 +SelfCheck 3600 +User clamav +Foreground yes +DetectPUA yes +# See https://github.com/vrtadmin/clamav-faq/blob/master/faq/faq-pua.md +#ExcludePUA NetTool +#ExcludePUA PWTool +#IncludePUA Spy +#IncludePUA Scanner +#IncludePUA RAT +HeuristicAlerts yes +ScanOLE2 yes +AlertOLE2Macros no +ScanPDF yes +ScanSWF yes +ScanXMLDOCS yes +ScanHWP3 yes +ScanMail yes +PhishingSignatures no +PhishingScanURLs no +HeuristicScanPrecedence yes +ScanHTML yes +ScanArchive yes +MaxScanSize 50M +MaxFileSize 25M +MaxRecursion 5 +MaxFiles 200 +Bytecode yes +BytecodeSecurity TrustSigned +BytecodeTimeout 1000 +ConcurrentDatabaseReload no diff --git a/mailcow/data/conf/clamav/freshclam.conf b/mailcow/data/conf/clamav/freshclam.conf new file mode 100644 index 0000000..cfb497e --- /dev/null +++ b/mailcow/data/conf/clamav/freshclam.conf @@ -0,0 +1,19 @@ +#UpdateLogFile /dev/console +LogTime yes +PidFile /run/clamav/freshclam.pid +DatabaseOwner clamav +DNSDatabaseInfo current.cvd.clamav.net +DatabaseMirror db.uk.clamav.net +DatabaseMirror db.nl.clamav.net +DatabaseMirror db.fr.clamav.net +DatabaseMirror db.ch.clamav.net +MaxAttempts 4 +ScriptedUpdates yes +Checks 6 +NotifyClamd /etc/clamav/clamd.conf +Foreground yes +ConnectTimeout 20 +ReceiveTimeout 20 +TestDatabases yes +Bytecode yes + diff --git a/mailcow/data/conf/dovecot/auth/mailcowauth.php b/mailcow/data/conf/dovecot/auth/mailcowauth.php new file mode 100644 index 0000000..fc17df7 --- /dev/null +++ b/mailcow/data/conf/dovecot/auth/mailcowauth.php @@ -0,0 +1,108 @@ + false); +if(!isset($post['username']) || !isset($post['password']) || !isset($post['real_rip'])){ + error_log("MAILCOWAUTH: Bad Request"); + http_response_code(400); // Bad Request + echo json_encode($return); + exit(); +} + +require_once('../../../web/inc/vars.inc.php'); +if (file_exists('../../../web/inc/vars.local.inc.php')) { + include_once('../../../web/inc/vars.local.inc.php'); +} +require_once '../../../web/inc/lib/vendor/autoload.php'; + + +// Init Redis +$redis = new Redis(); +try { + if (!empty(getenv('REDIS_SLAVEOF_IP'))) { + $redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT')); + } + else { + $redis->connect('redis-mailcow', 6379); + } + $redis->auth(getenv("REDISPASS")); +} +catch (Exception $e) { + error_log("MAILCOWAUTH: " . $e . PHP_EOL); + http_response_code(500); // Internal Server Error + echo json_encode($return); + exit; +} + +// Init database +$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name; +$opt = [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, +]; +try { + $pdo = new PDO($dsn, $database_user, $database_pass, $opt); +} +catch (PDOException $e) { + error_log("MAILCOWAUTH: " . $e . PHP_EOL); + http_response_code(500); // Internal Server Error + echo json_encode($return); + exit; +} + +// Load core functions first +require_once 'functions.inc.php'; +require_once 'functions.auth.inc.php'; +require_once 'sessions.inc.php'; +require_once 'functions.mailbox.inc.php'; +require_once 'functions.ratelimit.inc.php'; +require_once 'functions.acl.inc.php'; + + +$isSOGoRequest = $post['real_rip'] == getenv('IPV4_NETWORK') . '.248'; +$result = false; +$protocol = $post['protocol']; +if ($isSOGoRequest) { + $protocol = null; + // This is a SOGo Auth request. First check for SSO password. + $sogo_sso_pass = file_get_contents("/etc/sogo-sso/sogo-sso.pass"); + if ($sogo_sso_pass === $post['password']){ + error_log('MAILCOWAUTH: SOGo SSO auth for user ' . $post['username']); + $result = true; + } +} +if ($result === false){ + $result = apppass_login($post['username'], $post['password'], $protocol, array( + 'is_internal' => true, + 'remote_addr' => $post['real_rip'] + )); + if ($result) error_log('MAILCOWAUTH: App auth for user ' . $post['username']); +} +if ($result === false){ + // Init Identity Provider + $iam_provider = identity_provider('init'); + $iam_settings = identity_provider('get'); + $result = user_login($post['username'], $post['password'], array('is_internal' => true)); + if ($result) error_log('MAILCOWAUTH: User auth for user ' . $post['username']); +} + +if ($result) { + http_response_code(200); // OK + $return['success'] = true; +} else { + error_log("MAILCOWAUTH: Login failed for user " . $post['username']); + http_response_code(401); // Unauthorized +} + + +echo json_encode($return); +session_destroy(); +exit; diff --git a/mailcow/data/conf/dovecot/auth/passwd-verify.lua b/mailcow/data/conf/dovecot/auth/passwd-verify.lua new file mode 100644 index 0000000..cb2e928 --- /dev/null +++ b/mailcow/data/conf/dovecot/auth/passwd-verify.lua @@ -0,0 +1,42 @@ +function auth_password_verify(request, password) + if request.domain == nil then + return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No such user" + end + + json = require "cjson" + ltn12 = require "ltn12" + https = require "ssl.https" + https.TIMEOUT = 5 + + local req = { + username = request.user, + password = password, + real_rip = request.real_rip, + protocol = {} + } + req.protocol[request.service] = true + local req_json = json.encode(req) + local res = {} + + local b, c = https.request { + method = "POST", + url = "https://nginx:9082", + source = ltn12.source.string(req_json), + headers = { + ["content-type"] = "application/json", + ["content-length"] = tostring(#req_json) + }, + sink = ltn12.sink.table(res), + insecure = true + } + local api_response = json.decode(table.concat(res)) + if api_response.success == true then + return dovecot.auth.PASSDB_RESULT_OK, "" + end + + return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate" +end + +function auth_passdb_lookup(req) + return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "" +end diff --git a/mailcow/data/conf/dovecot/dovecot.conf b/mailcow/data/conf/dovecot/dovecot.conf new file mode 100644 index 0000000..15be77f --- /dev/null +++ b/mailcow/data/conf/dovecot/dovecot.conf @@ -0,0 +1,311 @@ +# -------------------------------------------------------------------------- +# Please create a file "extra.conf" for persistent overrides to dovecot.conf +# -------------------------------------------------------------------------- +# LDAP example: +#passdb { +# args = /etc/dovecot/ldap/passdb.conf +# driver = ldap +#} + +auth_mechanisms = plain login +#mail_debug = yes +#auth_debug = yes +#log_debug = category=fts-flatcurve # Activate Logging for Flatcurve FTS Searchings +log_path = syslog +disable_plaintext_auth = yes +# Uncomment on NFS share +#mmap_disable = yes +#mail_fsync = always +#mail_nfs_index = yes +#mail_nfs_storage = yes +login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c %k" +mail_home = /var/vmail/%d/%n +mail_location = maildir:~/ +mail_plugins = +!include_try /etc/dovecot/sni.conf +!include_try /etc/dovecot/sogo_trusted_ip.conf +!include_try /etc/dovecot/extra.conf +!include_try /etc/dovecot/shared_namespace.conf +!include_try /etc/dovecot/conf.d/fts.conf +# +default_client_limit = 10400 +default_vsz_limit = 1024 M diff --git a/mailcow/data/conf/dovecot/dovecot.folders.conf b/mailcow/data/conf/dovecot/dovecot.folders.conf new file mode 100644 index 0000000..fa68726 --- /dev/null +++ b/mailcow/data/conf/dovecot/dovecot.folders.conf @@ -0,0 +1,308 @@ +namespace inbox { + inbox = yes + location = + separator = / + mailbox "Trash" { + auto = subscribe + special_use = \Trash + } + mailbox "Deleted Messages" { + special_use = \Trash + } + mailbox "Deleted Items" { + special_use = \Trash + } + mailbox "Rubbish" { + special_use = \Trash + } + mailbox "Gelöschte Objekte" { + special_use = \Trash + } + mailbox "Gelöschte Elemente" { + special_use = \Trash + } + mailbox "Papierkorb" { + special_use = \Trash + } + mailbox "Itens Excluidos" { + special_use = \Trash + } + mailbox "Itens Excluídos" { + special_use = \Trash + } + mailbox "Lixeira" { + special_use = \Trash + } + mailbox "Prullenbak" { + special_use = \Trash + } + mailbox "Odstránené položky" { + special_use = \Trash + } + mailbox "KoÅ¡" { + special_use = \Trash + } + mailbox "Verwijderde items" { + special_use = \Trash + } + mailbox "Удаленные" { + special_use = \Trash + } + mailbox "Удаленные Ñлементы" { + special_use = \Trash + } + mailbox "Корзина" { + special_use = \Trash + } + mailbox "Видалені" { + special_use = \Trash + } + mailbox "Видалені елементи" { + special_use = \Trash + } + mailbox "Кошик" { + special_use = \Trash + } + mailbox "废件箱" { + special_use = \Trash + } + mailbox "已删除消æ¯" { + special_use = \Trash + } + mailbox "已删除邮件" { + special_use = \Trash + } + mailbox "Archive" { + auto = subscribe + special_use = \Archive + } + mailbox "Archiv" { + special_use = \Archive + } + mailbox "Archives" { + special_use = \Archive + } + mailbox "Arquivo" { + special_use = \Archive + } + mailbox "Arquivos" { + special_use = \Archive + } + mailbox "Archief" { + special_use = \Archive + } + mailbox "Archív" { + special_use = \Archive + } + mailbox "ArchivovaÅ¥" { + special_use = \Archive + } + mailbox "å½’æ¡£" { + special_use = \Archive + } + mailbox "Ðрхив" { + special_use = \Archive + } + mailbox "Ðрхів" { + special_use = \Archive + } + mailbox "Sent" { + auto = subscribe + special_use = \Sent + } + mailbox "Sent Messages" { + special_use = \Sent + } + mailbox "Sent Items" { + special_use = \Sent + } + mailbox "å·²å‘é€" { + special_use = \Sent + } + mailbox "å·²å‘逿¶ˆæ¯" { + special_use = \Sent + } + mailbox "å·²å‘é€é‚®ä»¶" { + special_use = \Sent + } + mailbox "Отправленные" { + special_use = \Sent + } + mailbox "Отправленные Ñлементы" { + special_use = \Sent + } + mailbox "ÐадіÑлані" { + special_use = \Sent + } + mailbox "ÐадіÑлані елементи" { + special_use = \Sent + } + mailbox "Gesendet" { + special_use = \Sent + } + mailbox "Gesendete Objekte" { + special_use = \Sent + } + mailbox "Gesendete Elemente" { + special_use = \Sent + } + mailbox "Itens Enviados" { + special_use = \Sent + } + mailbox "Enviados" { + special_use = \Sent + } + mailbox "Verzonden items" { + special_use = \Sent + } + mailbox "Verzonden" { + special_use = \Sent + } + mailbox "Odoslaná poÅ¡ta" { + special_use = \Sent + } + mailbox "Odoslané" { + special_use = \Sent + } + mailbox "Drafts" { + auto = subscribe + special_use = \Drafts + } + mailbox "Entwürfe" { + special_use = \Drafts + } + mailbox "Rascunhos" { + special_use = \Drafts + } + mailbox "Concepten" { + special_use = \Drafts + } + mailbox "Koncepty" { + special_use = \Drafts + } + mailbox "è‰ç¨¿" { + special_use = \Drafts + } + mailbox "è‰ç¨¿ç®±" { + special_use = \Drafts + } + mailbox "Черновики" { + special_use = \Drafts + } + mailbox "Чернетки" { + special_use = \Drafts + } + mailbox "Junk" { + auto = subscribe + special_use = \Junk + } + mailbox "Junk-E-Mail" { + special_use = \Junk + } + mailbox "Junk E-Mail" { + special_use = \Junk + } + mailbox "Spam" { + special_use = \Junk + } + mailbox "Lixo Eletrônico" { + special_use = \Junk + } + mailbox "Nevyžiadaná poÅ¡ta" { + special_use = \Junk + } + mailbox "Infikované položky" { + special_use = \Junk + } + mailbox "Ongewenste e-mail" { + special_use = \Junk + } + mailbox "垃圾" { + special_use = \Junk + } + mailbox "垃圾箱" { + special_use = \Junk + } + mailbox "ÐÐµÐ¶ÐµÐ»Ð°Ñ‚ÐµÐ»ÑŒÐ½Ð°Ñ Ð¿Ð¾Ñ‡Ñ‚Ð°" { + special_use = \Junk + } + mailbox "Спам" { + special_use = \Junk + } + mailbox "Ðебажана пошта" { + special_use = \Junk + } + mailbox "Koncepty" { + special_use = \Drafts + } + mailbox "Nevyžádaná poÅ¡ta" { + special_use = \Junk + } + mailbox "OdstranÄ›ná poÅ¡ta" { + special_use = \Trash + } + mailbox "Odeslaná poÅ¡ta" { + special_use = \Sent + } + mailbox "Skräp" { + special_use = \Trash + } + mailbox "Borttagna Meddelanden" { + special_use = \Trash + } + mailbox "Arkiv" { + special_use = \Archive + } + mailbox "Arkeverat" { + special_use = \Archive + } + mailbox "Skickat" { + special_use = \Sent + } + mailbox "Skickade Meddelanden" { + special_use = \Sent + } + mailbox "Utkast" { + special_use = \Drafts + } + mailbox "Skraldespand" { + special_use = \Trash + } + mailbox "Slettet mails" { + special_use = \Trash + } + mailbox "Arkiv" { + special_use = \Archive + } + mailbox "Arkiveret mails" { + special_use = \Archive + } + mailbox "Sendt" { + special_use = \Sent + } + mailbox "Sendte mails" { + special_use = \Sent + } + mailbox "Udkast" { + special_use = \Drafts + } + mailbox "Kladde" { + special_use = \Drafts + } + mailbox "ΠÏόχειÏα" { + special_use = \Drafts + } + mailbox "Απεσταλμένα" { + special_use = \Sent + } + mailbox "Κάδος αποÏÏιμάτων" { + special_use = \Trash + } + mailbox "ΑνεπιθÏμητα" { + special_use = \Junk + } + mailbox "ΑÏχειοθετημένα" { + special_use = \Archive + } + prefix = +} diff --git a/mailcow/data/conf/dovecot/ldap/passdb.conf b/mailcow/data/conf/dovecot/ldap/passdb.conf new file mode 100644 index 0000000..12fc3c0 --- /dev/null +++ b/mailcow/data/conf/dovecot/ldap/passdb.conf @@ -0,0 +1,9 @@ +#hosts = 1.2.3.4 +#dn = cn=admin,dc=example,dc=local +#dnpass = password +#ldap_version = 3 +#base = ou=People,dc=example,dc=local +#auth_bind = no +#pass_filter = (&(objectClass=posixAccount)(mail=%u)) +#pass_attrs = mail=user,userPassword=password +#default_pass_scheme = SSHA diff --git a/mailcow/data/conf/mysql/my.cnf b/mailcow/data/conf/mysql/my.cnf new file mode 100644 index 0000000..24d6123 --- /dev/null +++ b/mailcow/data/conf/mysql/my.cnf @@ -0,0 +1,35 @@ +[mysqld] +character-set-client-handshake = FALSE +character-set-server = utf8mb4 +collation-server = utf8mb4_general_ci +#innodb_file_per_table = TRUE +#innodb_file_format = barracuda +#innodb_large_prefix = TRUE +#sql_mode=IGNORE_SPACE,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION +max_allowed_packet = 192M +max-connections = 550 +key_buffer_size = 0 +read_buffer_size = 192K +sort_buffer_size = 2M +innodb_buffer_pool_size = 24M +read_rnd_buffer_size = 256K +tmp_table_size = 24M +performance_schema = 0 +innodb-strict-mode = 0 +thread_cache_size = 8 +query_cache_type = 0 +query_cache_size = 0 +max_heap_table_size = 48M +thread_stack = 256K +skip-host-cache +skip-name-resolve +log-warnings = 0 +event_scheduler = 1 +interactive_timeout = 3610 +wait_timeout = 3610 + +[client] +default-character-set = utf8mb4 + +[mysql] +default-character-set = utf8mb4 diff --git a/mailcow/data/conf/nginx/templates/nginx.conf.j2 b/mailcow/data/conf/nginx/templates/nginx.conf.j2 new file mode 100644 index 0000000..ff5f8f1 --- /dev/null +++ b/mailcow/data/conf/nginx/templates/nginx.conf.j2 @@ -0,0 +1,211 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + #gzip on; + + # map-size.conf: + map_hash_max_size 256; + map_hash_bucket_size 256; + + # site.conf: + proxy_cache_path /tmp levels=1:2 keys_zone=sogo:10m inactive=24h max_size=1g; + server_names_hash_max_size 512; + server_names_hash_bucket_size 128; + + map $http_x_forwarded_proto $client_req_scheme { + default $scheme; + https https; + } + + {% if HTTP_REDIRECT %} + # HTTP to HTTPS redirect + server { + root /web; + listen {{ HTTP_PORT }} default_server; + listen [::]:{{ HTTP_PORT }} default_server; + + server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* {{ ADDITIONAL_SERVER_NAMES | join(' ') }}; + + if ( $request_uri ~* "%0A|%0D" ) { return 403; } + location ^~ /.well-known/acme-challenge/ { + allow all; + default_type "text/plain"; + } + location / { + return 301 https://$host$uri$is_args$args; + } + } + {%endif%} + + # Default Server Name + server { + listen 127.0.0.1:65510; # sogo-auth verify internal + + {% if not HTTP_REDIRECT %} + listen {{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%}; + {%endif%} + listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl; + + {% if not DISABLE_IPv6 %} + {% if not HTTP_REDIRECT %} + listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%}; + {%endif%} + listen [::]:{{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl; + {%endif%} + + http2 on; + + ssl_certificate /etc/ssl/mail/cert.pem; + ssl_certificate_key /etc/ssl/mail/key.pem; + + server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.*; + + include /etc/nginx/includes/sites-default.conf; + } + + # Additional Server Names + {% for SERVER_NAME in ADDITIONAL_SERVER_NAMES %} + server { + listen 127.0.0.1:65510; # sogo-auth verify internal + + {% if not HTTP_REDIRECT %} + listen {{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%}; + {%endif%} + listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl; + + {% if not DISABLE_IPv6 %} + {% if not HTTP_REDIRECT %} + listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%}; + {%endif%} + listen [::]:{{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl; + {%endif%} + + http2 on; + + ssl_certificate /etc/ssl/mail/cert.pem; + ssl_certificate_key /etc/ssl/mail/key.pem; + + server_name {{ SERVER_NAME }}; + + include /etc/nginx/includes/sites-default.conf; + } + {% endfor %} + + # rspamd dynmaps: + server { + listen 8081; + {% if not DISABLE_IPv6 %} + listen [::]:8081; + {%endif%} + index index.php index.html; + server_name _; + error_log /var/log/nginx/error.log; + access_log /var/log/nginx/access.log; + root /dynmaps; + + location ~ \.php$ { + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass {{ PHPFPMHOST }}:9001; + fastcgi_index index.php; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + } + } + + # rspamd meta_exporter: + server { + listen 9081; + index index.php index.html; + server_name _; + error_log /var/log/nginx/error.log; + access_log /var/log/nginx/access.log; + root /meta_exporter; + client_max_body_size 10M; + location ~ \.php$ { + client_max_body_size 10M; + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass {{ PHPFPMHOST }}:9001; + fastcgi_index pipe.php; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + } + } + + server { + listen 9082 ssl http2; + + ssl_certificate /etc/ssl/mail/cert.pem; + ssl_certificate_key /etc/ssl/mail/key.pem; + + index mailcowauth.php; + server_name _; + error_log /var/log/nginx/error.log; + access_log /var/log/nginx/access.log; + root /mailcowauth; + client_max_body_size 10M; + location ~ \.php$ { + client_max_body_size 10M; + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass phpfpm:9001; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + } + } + + include /etc/nginx/conf.d/*.conf; + + {% for cert in valid_cert_dirs %} + server { + {% if not HTTP_REDIRECT %} + listen {{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%}; + {%endif%} + listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl; + + {% if not DISABLE_IPv6 %} + {% if not HTTP_REDIRECT %} + listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%}; + {%endif%} + listen [::]:{{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl; + {%endif%} + + http2 on; + + ssl_certificate {{ cert.cert_path }}cert.pem; + ssl_certificate_key {{ cert.cert_path }}key.pem; + + server_name {{ cert.domains }}; + + include /etc/nginx/includes/sites-default.conf; + } + {% endfor %} +} diff --git a/mailcow/data/conf/nginx/templates/sites-default.conf.j2 b/mailcow/data/conf/nginx/templates/sites-default.conf.j2 new file mode 100644 index 0000000..574bdb0 --- /dev/null +++ b/mailcow/data/conf/nginx/templates/sites-default.conf.j2 @@ -0,0 +1,287 @@ +include /etc/nginx/mime.types; +charset utf-8; +override_charset on; + +server_tokens off; + +ssl_protocols TLSv1.2 TLSv1.3; +ssl_prefer_server_ciphers on; +ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305; +ssl_ecdh_curve X25519:X448:secp384r1:secp256k1; +ssl_session_cache shared:SSL:50m; +ssl_session_timeout 1d; +ssl_session_tickets off; + +add_header Strict-Transport-Security "max-age=15768000;"; +add_header X-Content-Type-Options nosniff; +add_header X-XSS-Protection "1; mode=block"; +add_header X-Robots-Tag none; +add_header X-Download-Options noopen; +add_header X-Frame-Options "SAMEORIGIN" always; +add_header X-Permitted-Cross-Domain-Policies none; +add_header Referrer-Policy strict-origin; + +index index.php index.html; + +client_max_body_size 0; + +gzip on; +gzip_disable "msie6"; + +gzip_vary on; +gzip_proxied off; +gzip_comp_level 6; +gzip_buffers 16 8k; +gzip_http_version 1.1; +gzip_min_length 256; +gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon; + +location ~ ^/(fonts|js|css|img)/ { + expires max; + add_header Cache-Control public; +} + +error_log /var/log/nginx/error.log; +access_log /var/log/nginx/access.log; +fastcgi_hide_header X-Powered-By; +absolute_redirect off; +root /web; + +# If behind reverse proxy, forwards the correct IP +set_real_ip_from 10.0.0.0/8; +set_real_ip_from 172.16.0.0/12; +set_real_ip_from 192.168.0.0/16; +set_real_ip_from fc00::/7; +{% for TRUSTED_PROXY in TRUSTED_PROXIES %} +set_real_ip_from {{ TRUSTED_PROXY }}; +{% endfor %} +{% if not NGINX_USE_PROXY_PROTOCOL %} +real_ip_header X-Forwarded-For; +{% else %} +real_ip_header proxy_protocol; +{% endif %} +real_ip_recursive on; + + +location @strip-ext { + rewrite ^(.*)$ $1.php last; +} + +location ^~ /inc/lib/ { + deny all; + return 403; +} + +location ^~ /.well-known/acme-challenge/ { + allow all; + default_type "text/plain"; +} + +rewrite ^/.well-known/caldav$ /SOGo/dav/ permanent; +rewrite ^/.well-known/carddav$ /SOGo/dav/ permanent; + + +location / { + try_files $uri $uri/ @strip-ext; +} + +location /qhandler { + rewrite ^/qhandler/(.*)/(.*) /qhandler.php?action=$1&hash=$2; +} + +location /edit { + rewrite ^/edit/(.*)/(.*) /edit.php?$1=$2; +} + +location ~ ^/api/v1/(.*)$ { + try_files $uri $uri/ /json_api.php?query=$1&$args; +} + +location ~ ^/cache/(.*)$ { + try_files $uri $uri/ /resource.php?file=$1; +} + +location ~ \.php$ { + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass {{ PHPFPMHOST }}:9002; + fastcgi_index index.php; + include /etc/nginx/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_read_timeout 3600; + fastcgi_send_timeout 3600; +} + +location ~* ^/Autodiscover/Autodiscover.xml { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass {{ PHPFPMHOST }}:9002; + include /etc/nginx/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + try_files /autodiscover.php =404; +} + +location ~* ^/Autodiscover/Autodiscover.json { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass {{ PHPFPMHOST }}:9002; + include /etc/nginx/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + try_files /autodiscover-json.php =404; +} + +location ~ /(?:m|M)ail/(?:c|C)onfig-v1.1.xml { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass {{ PHPFPMHOST }}:9002; + include /etc/nginx/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + try_files /autoconfig.php =404; +} + +{% if not SKIP_RSPAMD %} +location /rspamd/ { + location /rspamd/auth { + # proxy_pass is not inherited + proxy_pass http://{{ RSPAMDHOST }}:11334/auth; + proxy_intercept_errors on; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For {% if not NGINX_USE_PROXY_PROTOCOL %}$proxy_add_x_forwarded_for{% else %}$proxy_protocol_addr{%endif%}; + proxy_set_header X-Real-IP {% if not NGINX_USE_PROXY_PROTOCOL %}$remote_addr{% else %}$proxy_protocol_addr{%endif%}; + proxy_redirect off; + error_page 401 /_rspamderror.php; + } + + proxy_pass http://{{ RSPAMDHOST }}:11334/; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For {% if not NGINX_USE_PROXY_PROTOCOL %}$proxy_add_x_forwarded_for{% else %}$proxy_protocol_addr{%endif%}; + proxy_set_header X-Real-IP {% if not NGINX_USE_PROXY_PROTOCOL %}$remote_addr{% else %}$proxy_protocol_addr{%endif%}; + proxy_redirect off; +} +{% endif %} + +{% if not SKIP_SOGO %} +location ^~ /principals { + return 301 /SOGo/dav; +} + +location /sogo-auth-verify { + internal; + proxy_set_header X-Original-URI $request_uri; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $http_host; + proxy_set_header Content-Length ""; + proxy_pass http://127.0.0.1:65510/sogo-auth; + proxy_pass_request_body off; +} + +location ^~ /Microsoft-Server-ActiveSync { + auth_request /sogo-auth-verify; + auth_request_set $user $upstream_http_x_user; + auth_request_set $auth $upstream_http_x_auth; + auth_request_set $auth_type $upstream_http_x_auth_type; + proxy_set_header x-webobjects-remote-user "$user"; + proxy_set_header Authorization "$auth"; + proxy_set_header x-webobjects-auth-type "$auth_type"; + + proxy_pass http://{{ SOGOHOST }}:20000/SOGo/Microsoft-Server-ActiveSync; + + proxy_set_header X-Forwarded-For {% if not NGINX_USE_PROXY_PROTOCOL %}$proxy_add_x_forwarded_for{% else %}$proxy_protocol_addr{%endif%}; + proxy_set_header X-Real-IP {% if not NGINX_USE_PROXY_PROTOCOL %}$remote_addr{% else %}$proxy_protocol_addr{%endif%}; + proxy_connect_timeout 75; + proxy_send_timeout 3600; + proxy_read_timeout 3600; + proxy_buffer_size 128k; + proxy_buffers 64 512k; + proxy_busy_buffers_size 512k; + proxy_set_header Host $http_host; + client_body_buffer_size 512k; + client_max_body_size 0; +} + +location ^~ /SOGo { + location ~* ^/SOGo/so/.*\.(xml|js|html|xhtml)$ { + auth_request /sogo-auth-verify; + auth_request_set $user $upstream_http_x_user; + auth_request_set $auth $upstream_http_x_auth; + auth_request_set $auth_type $upstream_http_x_auth_type; + proxy_set_header x-webobjects-remote-user "$user"; + proxy_set_header Authorization "$auth"; + proxy_set_header x-webobjects-auth-type "$auth_type"; + + proxy_pass http://{{ SOGOHOST }}:20000; + + proxy_set_header X-Forwarded-For {% if not NGINX_USE_PROXY_PROTOCOL %}$proxy_add_x_forwarded_for{% else %}$proxy_protocol_addr{%endif%}; + proxy_set_header X-Real-IP {% if not NGINX_USE_PROXY_PROTOCOL %}$remote_addr{% else %}$proxy_protocol_addr{%endif%}; + proxy_set_header Host $http_host; + proxy_set_header x-webobjects-server-protocol HTTP/1.0; + proxy_set_header x-webobjects-remote-host $remote_addr; + proxy_set_header x-webobjects-server-name $server_name; + proxy_set_header x-webobjects-server-url $client_req_scheme://$http_host; + proxy_set_header x-webobjects-server-port $server_port; + proxy_hide_header Content-Type; + add_header Content-Type text/plain; + break; + } + auth_request /sogo-auth-verify; + auth_request_set $user $upstream_http_x_user; + auth_request_set $auth $upstream_http_x_auth; + auth_request_set $auth_type $upstream_http_x_auth_type; + proxy_set_header x-webobjects-remote-user "$user"; + proxy_set_header Authorization "$auth"; + proxy_set_header x-webobjects-auth-type "$auth_type"; + + proxy_pass http://{{ SOGOHOST }}:20000; + + proxy_set_header X-Forwarded-For {% if not NGINX_USE_PROXY_PROTOCOL %}$proxy_add_x_forwarded_for{% else %}$proxy_protocol_addr{%endif%}; + proxy_set_header X-Real-IP {% if not NGINX_USE_PROXY_PROTOCOL %}$remote_addr{% else %}$proxy_protocol_addr{%endif%}; + proxy_set_header Host $http_host; + proxy_set_header x-webobjects-server-protocol HTTP/1.0; + proxy_set_header x-webobjects-remote-host $remote_addr; + proxy_set_header x-webobjects-server-name $server_name; + proxy_set_header x-webobjects-server-url $client_req_scheme://$http_host; + proxy_set_header x-webobjects-server-port $server_port; + proxy_buffer_size 128k; + proxy_buffers 64 512k; + proxy_busy_buffers_size 512k; + proxy_send_timeout 3600; + proxy_read_timeout 3600; + client_body_buffer_size 128k; + client_max_body_size 0; + break; +} + +location ~* /sogo$ { + return 301 $client_req_scheme://$http_host/SOGo; +} + +location /SOGo.woa/WebServerResources/ { + alias /usr/lib/GNUstep/SOGo/WebServerResources/; +} + +location /.woa/WebServerResources/ { + alias /usr/lib/GNUstep/SOGo/WebServerResources/; +} + +location /SOGo/WebServerResources/ { + alias /usr/lib/GNUstep/SOGo/WebServerResources/; +} + +location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$) { + alias /usr/lib/GNUstep/SOGo/$1.SOGo/Resources/$2; +} +{% endif %} + + +include /etc/nginx/conf.d/site.*.custom; + +error_page 502 @awaitingupstream; + +location @awaitingupstream { + rewrite ^(.*)$ /_status.502.html break; +} + +location ~* \.php$ { + return 404; +} +location ~* \.twig$ { + return 404; +} diff --git a/mailcow/data/conf/phpfpm/crons/keycloak-sync.php b/mailcow/data/conf/phpfpm/crons/keycloak-sync.php new file mode 100644 index 0000000..c9655a8 --- /dev/null +++ b/mailcow/data/conf/phpfpm/crons/keycloak-sync.php @@ -0,0 +1,231 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, +]; +try { + $pdo = new PDO($dsn, $database_user, $database_pass, $opt); +} +catch (PDOException $e) { + logMsg("err", $e->getMessage()); + session_destroy(); + exit; +} + +// Init Redis +$redis = new Redis(); +try { + if (!empty(getenv('REDIS_SLAVEOF_IP'))) { + $redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT')); + } + else { + $redis->connect('redis-mailcow', 6379); + } + $redis->auth(getenv("REDISPASS")); +} +catch (Exception $e) { + echo "Exiting: " . $e->getMessage(); + session_destroy(); + exit; +} + +function logMsg($priority, $message, $task = "Keycloak Sync") { + global $redis; + + $finalMsg = array( + "time" => time(), + "priority" => $priority, + "task" => $task, + "message" => $message + ); + $redis->lPush('CRON_LOG', json_encode($finalMsg)); +} + +// Load core functions first +require_once __DIR__ . '/../web/inc/functions.inc.php'; +require_once __DIR__ . '/../web/inc/functions.auth.inc.php'; +require_once __DIR__ . '/../web/inc/sessions.inc.php'; +require_once __DIR__ . '/../web/inc/functions.mailbox.inc.php'; +require_once __DIR__ . '/../web/inc/functions.ratelimit.inc.php'; +require_once __DIR__ . '/../web/inc/functions.acl.inc.php'; + +$_SESSION['mailcow_cc_username'] = "admin"; +$_SESSION['mailcow_cc_role'] = "admin"; +$_SESSION['acl']['tls_policy'] = "1"; +$_SESSION['acl']['quarantine_notification'] = "1"; +$_SESSION['acl']['quarantine_category'] = "1"; +$_SESSION['acl']['ratelimit'] = "1"; +$_SESSION['acl']['sogo_access'] = "1"; +$_SESSION['acl']['protocol_access'] = "1"; +$_SESSION['acl']['mailbox_relayhost'] = "1"; +$_SESSION['acl']['unlimited_quota'] = "1"; + +$iam_settings = identity_provider('get'); +if ($iam_settings['authsource'] != "keycloak" || (intval($iam_settings['periodic_sync']) != 1 && intval($iam_settings['import_users']) != 1)) { + session_destroy(); + exit; +} + +// Set pagination variables +$start = 0; +$max = 100; + +// lock sync if already running +$lock_file = '/tmp/iam-sync.lock'; +if (file_exists($lock_file)) { + $lock_file_parts = explode("\n", file_get_contents($lock_file)); + $pid = $lock_file_parts[0]; + if (count($lock_file_parts) > 1){ + $last_execution = $lock_file_parts[1]; + $elapsed_time = (time() - $last_execution) / 60; + if ($elapsed_time < intval($iam_settings['sync_interval'])) { + logMsg("warning", "Sync not ready (".number_format((float)$elapsed_time, 2, '.', '')."min / ".$iam_settings['sync_interval']."min)"); + session_destroy(); + exit; + } + } + + if (posix_kill($pid, 0)) { + logMsg("warning", "Sync is already running"); + session_destroy(); + exit; + } else { + unlink($lock_file); + } +} +$lock_file_handle = fopen($lock_file, 'w'); +fwrite($lock_file_handle, getmypid()); +fclose($lock_file_handle); + +// Init Keycloak Provider +$iam_provider = identity_provider('init'); + +// Loop until all users have been retrieved +while (true) { + // Get admin access token + $admin_token = identity_provider("get-keycloak-admin-token"); + + // Make the API request to retrieve the users + $url = "{$iam_settings['server_url']}/admin/realms/{$iam_settings['realm']}/users?first=$start&max=$max"; + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "Content-Type: application/json", + "Authorization: Bearer " . $admin_token + ]); + $response = curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($code != 200){ + logMsg("err", "Received HTTP {$code}"); + session_destroy(); + exit; + } + try { + $response = json_decode($response, true); + } catch (Exception $e) { + logMsg("err", $e->getMessage()); + break; + } + if (!is_array($response)){ + logMsg("err", "Received malformed response from keycloak api"); + break; + } + if (count($response) == 0) { + break; + } + + // Process the batch of users + foreach ($response as $user) { + if (empty($user['email'])){ + logMsg("warning", "No email address in keycloak found for user " . $user['name']); + continue; + } + + // try get mailbox user + $stmt = $pdo->prepare("SELECT + mailbox.*, + domain.active AS d_active + FROM `mailbox` + INNER JOIN domain on mailbox.domain = domain.domain + WHERE `kind` NOT REGEXP 'location|thing|group' + AND `username` = :user"); + $stmt->execute(array(':user' => $user['email'])); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + + // check if matching attribute mapping exists + $user_template = $user['attributes']['mailcow_template'][0]; + $mapper_key = array_search($user_template, $iam_settings['mappers']); + + $_SESSION['access_all_exception'] = '1'; + if (!$row && intval($iam_settings['import_users']) == 1){ + if ($mapper_key === false){ + if (!empty($iam_settings['default_template'])) { + $mbox_template = $iam_settings['default_template']; + logMsg("warning", "Using default template for user " . $user['email']); + } else { + logMsg("warning", "No matching attribute mapping found for user " . $user['email']); + continue; + } + } else { + $mbox_template = $iam_settings['templates'][$mapper_key]; + } + // mailbox user does not exist, create... + logMsg("info", "Creating user " . $user['email']); + $create_res = mailbox('add', 'mailbox_from_template', array( + 'domain' => explode('@', $user['email'])[1], + 'local_part' => explode('@', $user['email'])[0], + 'name' => $user['firstName'] . " " . $user['lastName'], + 'authsource' => 'keycloak', + 'template' => $mbox_template + )); + if (!$create_res){ + logMsg("err", "Could not create user " . $user['email']); + continue; + } + } else if ($row && intval($iam_settings['periodic_sync']) == 1) { + if ($mapper_key === false){ + logMsg("warning", "No matching attribute mapping found for user " . $user['email']); + continue; + } + $mbox_template = $iam_settings['templates'][$mapper_key]; + // mailbox user does exist, sync attribtues... + logMsg("info", "Syncing attributes for user " . $user['email']); + mailbox('edit', 'mailbox_from_template', array( + 'username' => $user['email'], + 'name' => $user['firstName'] . " " . $user['lastName'], + 'template' => $mbox_template + )); + } else { + // skip mailbox user + logMsg("info", "Skipping user " . $user['email']); + } + $_SESSION['access_all_exception'] = '0'; + + sleep(0.025); + } + + // Update the pagination variables for the next batch + $start += $max; + sleep(1); +} + +logMsg("info", "DONE!"); +// add last execution time to lock file +$lock_file_handle = fopen($lock_file, 'w'); +fwrite($lock_file_handle, getmypid() . "\n" . time()); +fclose($lock_file_handle); +session_destroy(); diff --git a/mailcow/data/conf/phpfpm/crons/ldap-sync.php b/mailcow/data/conf/phpfpm/crons/ldap-sync.php new file mode 100644 index 0000000..66b76e6 --- /dev/null +++ b/mailcow/data/conf/phpfpm/crons/ldap-sync.php @@ -0,0 +1,198 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, +]; +try { + $pdo = new PDO($dsn, $database_user, $database_pass, $opt); +} +catch (PDOException $e) { + logMsg("err", $e->getMessage()); + session_destroy(); + exit; +} + +// Init Redis +$redis = new Redis(); +try { + if (!empty(getenv('REDIS_SLAVEOF_IP'))) { + $redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT')); + } + else { + $redis->connect('redis-mailcow', 6379); + } + $redis->auth(getenv("REDISPASS")); +} +catch (Exception $e) { + echo "Exiting: " . $e->getMessage(); + session_destroy(); + exit; +} + +function logMsg($priority, $message, $task = "LDAP Sync") { + global $redis; + + $finalMsg = array( + "time" => time(), + "priority" => $priority, + "task" => $task, + "message" => $message + ); + $redis->lPush('CRON_LOG', json_encode($finalMsg)); +} + +// Load core functions first +require_once __DIR__ . '/../web/inc/functions.inc.php'; +require_once __DIR__ . '/../web/inc/functions.auth.inc.php'; +require_once __DIR__ . '/../web/inc/sessions.inc.php'; +require_once __DIR__ . '/../web/inc/functions.mailbox.inc.php'; +require_once __DIR__ . '/../web/inc/functions.ratelimit.inc.php'; +require_once __DIR__ . '/../web/inc/functions.acl.inc.php'; + +$_SESSION['mailcow_cc_username'] = "admin"; +$_SESSION['mailcow_cc_role'] = "admin"; +$_SESSION['acl']['tls_policy'] = "1"; +$_SESSION['acl']['quarantine_notification'] = "1"; +$_SESSION['acl']['quarantine_category'] = "1"; +$_SESSION['acl']['ratelimit'] = "1"; +$_SESSION['acl']['sogo_access'] = "1"; +$_SESSION['acl']['protocol_access'] = "1"; +$_SESSION['acl']['mailbox_relayhost'] = "1"; +$_SESSION['acl']['unlimited_quota'] = "1"; + +$iam_settings = identity_provider('get'); +if ($iam_settings['authsource'] != "ldap" || (intval($iam_settings['periodic_sync']) != 1 && intval($iam_settings['import_users']) != 1)) { + session_destroy(); + exit; +} + +// Set pagination variables +$start = 0; +$max = 100; + +// lock sync if already running +$lock_file = '/tmp/iam-sync.lock'; +if (file_exists($lock_file)) { + $lock_file_parts = explode("\n", file_get_contents($lock_file)); + $pid = $lock_file_parts[0]; + if (count($lock_file_parts) > 1){ + $last_execution = $lock_file_parts[1]; + $elapsed_time = (time() - $last_execution) / 60; + if ($elapsed_time < intval($iam_settings['sync_interval'])) { + logMsg("warning", "Sync not ready (".number_format((float)$elapsed_time, 2, '.', '')."min / ".$iam_settings['sync_interval']."min)"); + session_destroy(); + exit; + } + } + + if (posix_kill($pid, 0)) { + logMsg("warning", "Sync is already running"); + session_destroy(); + exit; + } else { + unlink($lock_file); + } +} +$lock_file_handle = fopen($lock_file, 'w'); +fwrite($lock_file_handle, getmypid()); +fclose($lock_file_handle); + +// Init Provider +$iam_provider = identity_provider('init'); + +// Get ldap users +$ldap_query = $iam_provider->query(); +if (!empty($iam_settings['filter'])) { + $ldap_query = $ldap_query->rawFilter($iam_settings['filter']); +} +$response = $ldap_query->where($iam_settings['username_field'], "*") + ->where($iam_settings['attribute_field'], "*") + ->select([$iam_settings['username_field'], $iam_settings['attribute_field'], 'displayname']) + ->paginate($max); + +// Process the users +foreach ($response as $user) { + // try get mailbox user + $stmt = $pdo->prepare("SELECT + mailbox.*, + domain.active AS d_active + FROM `mailbox` + INNER JOIN domain on mailbox.domain = domain.domain + WHERE `kind` NOT REGEXP 'location|thing|group' + AND `username` = :user"); + $stmt->execute(array(':user' => $user[$iam_settings['username_field']][0])); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + + // check if matching attribute mapping exists + $user_template = $user[$iam_settings['attribute_field']][0]; + $mapper_key = array_search($user_template, $iam_settings['mappers']); + + if (empty($user[$iam_settings['username_field']][0])){ + logMsg("warning", "Skipping user " . $user['displayname'][0] . " due to empty LDAP ". $iam_settings['username_field'] . " property."); + continue; + } + + $_SESSION['access_all_exception'] = '1'; + if (!$row && intval($iam_settings['import_users']) == 1){ + if ($mapper_key === false){ + if (!empty($iam_settings['default_template'])) { + $mbox_template = $iam_settings['default_template']; + } else { + logMsg("warning", "No matching attribute mapping found for user " . $user[$iam_settings['username_field']][0]); + continue; + } + } else { + $mbox_template = $iam_settings['templates'][$mapper_key]; + } + // mailbox user does not exist, create... + logMsg("info", "Creating user " . $user[$iam_settings['username_field']][0]); + $create_res = mailbox('add', 'mailbox_from_template', array( + 'domain' => explode('@', $user[$iam_settings['username_field']][0])[1], + 'local_part' => explode('@', $user[$iam_settings['username_field']][0])[0], + 'name' => $user['displayname'][0], + 'authsource' => 'ldap', + 'template' => $mbox_template + )); + if (!$create_res){ + logMsg("err", "Could not create user " . $user[$iam_settings['username_field']][0]); + continue; + } + } else if ($row && intval($iam_settings['periodic_sync']) == 1) { + if ($mapper_key === false){ + logMsg("warning", "No matching attribute mapping found for user " . $user[$iam_settings['username_field']][0]); + continue; + } + $mbox_template = $iam_settings['templates'][$mapper_key]; + // mailbox user does exist, sync attribtues... + logMsg("info", "Syncing attributes for user " . $user[$iam_settings['username_field']][0]); + mailbox('edit', 'mailbox_from_template', array( + 'username' => $user[$iam_settings['username_field']][0], + 'name' => $user['displayname'][0], + 'template' => $mbox_template + )); + } else { + // skip mailbox user + logMsg("info", "Skipping user " . $user[$iam_settings['username_field']][0]); + } + $_SESSION['access_all_exception'] = '0'; + + sleep(0.025); +} + +logMsg("info", "DONE!"); +// add last execution time to lock file +$lock_file_handle = fopen($lock_file, 'w'); +fwrite($lock_file_handle, getmypid() . "\n" . time()); +fclose($lock_file_handle); +session_destroy(); diff --git a/mailcow/data/conf/phpfpm/php-conf.d/opcache-recommended.ini b/mailcow/data/conf/phpfpm/php-conf.d/opcache-recommended.ini new file mode 100644 index 0000000..3c51e11 --- /dev/null +++ b/mailcow/data/conf/phpfpm/php-conf.d/opcache-recommended.ini @@ -0,0 +1,7 @@ +opcache.enable=1 +opcache.enable_cli=1 +opcache.interned_strings_buffer=16 +opcache.max_accelerated_files=10000 +opcache.memory_consumption=128 +opcache.save_comments=1 +opcache.revalidate_freq=1 diff --git a/mailcow/data/conf/phpfpm/php-conf.d/other.ini b/mailcow/data/conf/phpfpm/php-conf.d/other.ini new file mode 100644 index 0000000..02f59a9 --- /dev/null +++ b/mailcow/data/conf/phpfpm/php-conf.d/other.ini @@ -0,0 +1,3 @@ +max_execution_time = 3600 +max_input_time = 3600 +memory_limit = 512M diff --git a/mailcow/data/conf/phpfpm/php-conf.d/upload.ini b/mailcow/data/conf/phpfpm/php-conf.d/upload.ini new file mode 100644 index 0000000..5cb28f1 --- /dev/null +++ b/mailcow/data/conf/phpfpm/php-conf.d/upload.ini @@ -0,0 +1,3 @@ +file_uploads = On +upload_max_filesize = 64M +post_max_size = 64M diff --git a/mailcow/data/conf/phpfpm/php-fpm.d/pools.conf b/mailcow/data/conf/phpfpm/php-fpm.d/pools.conf new file mode 100644 index 0000000..605e686 --- /dev/null +++ b/mailcow/data/conf/phpfpm/php-fpm.d/pools.conf @@ -0,0 +1,29 @@ +[system-worker] +user = www-data +group = www-data +pm = dynamic +pm.max_children = 15 +pm.start_servers = 2 +pm.min_spare_servers = 2 +pm.max_spare_servers = 4 +listen = [::]:9001 +access.log = /proc/self/fd/2 +clear_env = no +catch_workers_output = yes +php_admin_value[memory_limit] = 256M +php_admin_value[disable_functions] = show_source, highlight_file, apache_child_terminate, apache_get_modules, apache_note, apache_setenv, virtual, dl, disk_total_space, posix_getpwnam, posix_getpwuid, posix_mkfifo, posix_mknod, posix_setpgid, posix_setsid, posix_setuid, posix_uname, proc_nice, openlog, syslog, pfsockopen, system, shell_exec, passthru, popen, proc_open, exec, ini_alter, pcntl_exec, proc_close, proc_get_status, proc_terminate, symlink + +[web-worker] +user = www-data +group = www-data +pm = dynamic +pm.max_children = 50 +pm.start_servers = 10 +pm.min_spare_servers = 10 +pm.max_spare_servers = 15 +listen = [::]:9002 +access.log = /proc/self/fd/2 +clear_env = no +catch_workers_output = yes +php_admin_value[memory_limit] = 512M +php_admin_value[disable_functions] = show_source, highlight_file, apache_child_terminate, apache_get_modules, apache_note, apache_setenv, virtual, dl, disk_total_space, posix_getpwnam, posix_getpwuid, posix_mkfifo, posix_mknod, posix_setpgid, posix_setsid, posix_setuid, posix_uname, proc_nice, openlog, syslog, pfsockopen, system, shell_exec, passthru, popen, proc_open, exec, ini_alter, pcntl_exec, proc_close, proc_get_status, proc_terminate, symlink diff --git a/mailcow/data/conf/phpfpm/sogo-sso/.gitkeep b/mailcow/data/conf/phpfpm/sogo-sso/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/mailcow/data/conf/postfix/anonymize_headers.pcre b/mailcow/data/conf/postfix/anonymize_headers.pcre new file mode 100644 index 0000000..1a59d53 --- /dev/null +++ b/mailcow/data/conf/postfix/anonymize_headers.pcre @@ -0,0 +1,20 @@ +if /^\s*Received:.*Authenticated sender.*\(Postcow\)/ +#/^Received: from .*? \([\w-.]* \[.*?\]\)\s+\(Authenticated sender: (.+)\)\s+by.+\(Postcow\) with (E?SMTPS?A?) id ([A-F0-9]+).+;.*?/ +/^Received: from .*? \([\w\-.]* \[.*?\]\)(.*|\n.*)\(Authenticated sender: (.+)\)\s+by.+\(Postcow\) with (.*)/ + REPLACE Received: from [127.0.0.1] (localhost [127.0.0.1]) by localhost (Mailerdaemon) with $3 +endif +if /^\s*Received: from.* \(.*dovecot-mailcow.*mailcow-network.*\).*\(Postcow\)/ +/^Received: from.* (.*|\n.*)\((.+) (.+)\)\s+by (.+) \(Postcow\) with (.*)/ + REPLACE Received: from sieve (sieve $3) by $4 (Postcow) with $5 +endif +if /^\s*Received: from.* \(.*rspamd-mailcow.*mailcow-network.*\).*\(Postcow\)/ +/^Received: from.* (.*|\n.*)\((.+) (.+)\)\s+by (.+) \(Postcow\) with (.*)/ + REPLACE Received: from rspamd (rspamd $3) by $4 (Postcow) with $5 +endif +/^\s*X-Enigmail/ IGNORE +# Not removing Mailer by default, might be signed +#/^\s*X-Mailer/ IGNORE +/^\s*X-Originating-IP/ IGNORE +/^\s*X-Forward/ IGNORE +# Not removing UA by default, might be signed +#/^\s*User-Agent/ IGNORE diff --git a/mailcow/data/conf/postfix/local_transport b/mailcow/data/conf/postfix/local_transport new file mode 100644 index 0000000..6dd2101 --- /dev/null +++ b/mailcow/data/conf/postfix/local_transport @@ -0,0 +1,2 @@ +/watchdog@localhost$/ watchdog_discard: +/localhost$/ local: diff --git a/mailcow/data/conf/postfix/main.cf b/mailcow/data/conf/postfix/main.cf new file mode 100644 index 0000000..f9fd7e3 --- /dev/null +++ b/mailcow/data/conf/postfix/main.cf @@ -0,0 +1,202 @@ +# -------------------------------------------------------------------------- +# Please create a file "extra.cf" for persistent overrides to main.cf +# -------------------------------------------------------------------------- +biff = no +append_dot_mydomain = no +smtpd_tls_cert_file = /etc/ssl/mail/cert.pem +smtpd_tls_key_file = /etc/ssl/mail/key.pem +tls_server_sni_maps = hash:/opt/postfix/conf/sni.map +smtpd_tls_received_header = yes +smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache +smtpd_relay_restrictions = permit_mynetworks, + permit_sasl_authenticated, + defer_unauth_destination +smtpd_forbid_bare_newline = yes +# alias maps are auto-generated in postfix.sh on startup +alias_maps = hash:/etc/aliases +alias_database = hash:/etc/aliases +relayhost = +mynetworks_style = subnet +mailbox_size_limit = 0 +recipient_delimiter = + +inet_interfaces = all +inet_protocols = all +bounce_queue_lifetime = 1d +broken_sasl_auth_clients = yes +disable_vrfy_command = yes +maximal_backoff_time = 1800s +maximal_queue_lifetime = 5d +delay_warning_time = 4h +message_size_limit = 104857600 +milter_default_action = tempfail +milter_protocol = 6 +minimal_backoff_time = 300s +plaintext_reject_code = 550 +postscreen_access_list = permit_mynetworks, + cidr:/opt/postfix/conf/custom_postscreen_whitelist.cidr, + cidr:/opt/postfix/conf/postscreen_access.cidr, + tcp:127.0.0.1:10027 +postscreen_bare_newline_enable = no +postscreen_blacklist_action = drop +postscreen_cache_cleanup_interval = 24h +postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache +postscreen_dnsbl_action = enforce +postscreen_dnsbl_threshold = 6 +postscreen_dnsbl_ttl = 5m +postscreen_greet_action = enforce +postscreen_greet_banner = $smtpd_banner +postscreen_greet_ttl = 2d +postscreen_greet_wait = 3s +postscreen_non_smtp_command_enable = no +postscreen_pipelining_enable = no +proxy_read_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_transport_maps.cf, + proxy:mysql:/opt/postfix/conf/sql/mysql_mbr_access_maps.cf, + proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf, + $sender_dependent_default_transport_maps, + $smtp_tls_policy_maps, + $local_recipient_maps, + $mydestination, + $virtual_alias_maps, + $virtual_alias_domains, + $virtual_mailbox_maps, + $virtual_mailbox_domains, + $relay_recipient_maps, + $relay_domains, + $canonical_maps, + $sender_canonical_maps, + $sender_bcc_maps, + $recipient_bcc_maps, + $recipient_canonical_maps, + $relocated_maps, + $transport_maps, + $mynetworks, + $smtpd_sender_login_maps, + $smtp_sasl_password_maps +queue_run_delay = 300s +relay_domains = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_relay_domain_maps.cf +relay_recipient_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_relay_recipient_maps.cf +sender_dependent_default_transport_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sender_dependent_default_transport_maps.cf +smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt +smtp_tls_cert_file = /etc/ssl/mail/cert.pem +smtp_tls_key_file = /etc/ssl/mail/key.pem +smtp_tls_loglevel = 1 +smtp_dns_support_level = dnssec +smtp_tls_security_level = dane +smtpd_data_restrictions = reject_unauth_pipelining, permit +smtpd_delay_reject = yes +smtpd_error_sleep_time = 10s +smtpd_forbid_bare_newline = yes +smtpd_hard_error_limit = ${stress?1}${stress:5} +smtpd_helo_required = yes +smtpd_proxy_timeout = 600s +smtpd_recipient_restrictions = check_recipient_mx_access proxy:mysql:/opt/postfix/conf/sql/mysql_mbr_access_maps.cf, + permit_sasl_authenticated, + permit_mynetworks, + check_recipient_access proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf, + reject_invalid_helo_hostname, + reject_unauth_destination +smtpd_sasl_auth_enable = yes +smtpd_sasl_authenticated_header = yes +smtpd_sasl_path = inet:dovecot:10001 +smtpd_sasl_type = dovecot +smtpd_sender_login_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_sender_acl.cf +smtpd_sender_restrictions = reject_authenticated_sender_login_mismatch, + permit_mynetworks, + permit_sasl_authenticated, + reject_unlisted_sender, + reject_unknown_sender_domain +smtpd_soft_error_limit = 3 +smtpd_tls_auth_only = yes +smtpd_tls_dh1024_param_file = /etc/ssl/mail/dhparams.pem +smtpd_tls_eecdh_grade = auto +smtpd_tls_exclude_ciphers = ECDHE-RSA-RC4-SHA, RC4, aNULL, DES-CBC3-SHA, ECDHE-RSA-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA +smtpd_tls_loglevel = 1 + +# Mandatory protocols and ciphers are used when a connections is enforced to use TLS +# Does _not_ apply to enforced incoming TLS settings per mailbox +smtp_tls_mandatory_protocols = >=TLSv1.2 +lmtp_tls_mandatory_protocols = >=TLSv1.2 +smtpd_tls_mandatory_protocols = >=TLSv1.2 +smtpd_tls_mandatory_ciphers = high + +smtp_tls_protocols = >=TLSv1.2 +lmtp_tls_protocols = >=TLSv1.2 +smtpd_tls_protocols = >=TLSv1.2 + +smtpd_tls_security_level = may +tls_preempt_cipherlist = yes +tls_ssl_options = NO_COMPRESSION, NO_RENEGOTIATION +virtual_alias_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_maps.cf, + proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_resource_maps.cf, + proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_spamalias_maps.cf, + proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf +virtual_gid_maps = static:5000 +virtual_mailbox_base = /var/vmail/ +virtual_mailbox_domains = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_domains_maps.cf +# -- moved to rspamd on 2021-06-01 +#recipient_bcc_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_recipient_bcc_maps.cf +#sender_bcc_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sender_bcc_maps.cf +recipient_canonical_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_recipient_canonical_maps.cf +recipient_canonical_classes = envelope_recipient +virtual_mailbox_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_mailbox_maps.cf +virtual_minimum_uid = 104 +virtual_transport = lmtp:inet:dovecot:24 +virtual_uid_maps = static:5000 +smtpd_milters = inet:rspamd:9900 +non_smtpd_milters = inet:rspamd:9900 +milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen} +mydestination = localhost.localdomain, localhost +smtp_address_preference = any +smtp_sender_dependent_authentication = yes +smtp_sasl_auth_enable = yes +smtp_sasl_password_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf +smtp_sasl_security_options = +smtp_sasl_mechanism_filter = plain, login +smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf +smtp_header_checks = pcre:/opt/postfix/conf/anonymize_headers.pcre +mail_name = Postcow +# local_transport map catches local destinations and prevents routing local dests when the next map would route "*" +# Use custom_transport.pcre for custom transports +transport_maps = pcre:/opt/postfix/conf/custom_transport.pcre, + pcre:/opt/postfix/conf/local_transport, + proxy:mysql:/opt/postfix/conf/sql/mysql_relay_ne.cf, + proxy:mysql:/opt/postfix/conf/sql/mysql_transport_maps.cf +smtp_sasl_auth_soft_bounce = no +postscreen_discard_ehlo_keywords = chunking, silent-discard, smtputf8, dsn +smtpd_discard_ehlo_keywords = chunking, silent-discard, smtputf8 +compatibility_level = 3.7 +# Define protocols for SMTPS and submission service +submission_smtpd_tls_mandatory_protocols = >=TLSv1.2 +smtps_smtpd_tls_mandatory_protocols = >=TLSv1.2 +parent_domain_matches_subdomains = debug_peer_list,fast_flush_domains,mynetworks,qmqpd_authorized_clients +# This Option is added to correctly set the X-Original-To Header when mails are send to lmtp (dovecot) +lmtp_destination_recipient_limit=1 + +# DO NOT EDIT ANYTHING BELOW # +# Overrides # + +postscreen_dnsbl_sites = wl.mailspike.net=127.0.0.[18;19;20]*-2 + hostkarma.junkemailfilter.com=127.0.0.1*-2 + list.dnswl.org=127.0.[0..255].0*-2 + list.dnswl.org=127.0.[0..255].1*-4 + list.dnswl.org=127.0.[0..255].2*-6 + list.dnswl.org=127.0.[0..255].3*-8 + bl.spamcop.net*2 + bl.suomispam.net*2 + hostkarma.junkemailfilter.com=127.0.0.2*3 + hostkarma.junkemailfilter.com=127.0.0.4*2 + hostkarma.junkemailfilter.com=127.0.1.2*1 + backscatter.spameatingmonkey.net*2 + bl.ipv6.spameatingmonkey.net*2 + bl.spameatingmonkey.net*2 + b.barracudacentral.org=127.0.0.2*7 + bl.mailspike.net=127.0.0.2*5 + bl.mailspike.net=127.0.0.[10;11;12]*4 + zen.spamhaus.org=127.0.0.[10;11]*8 + zen.spamhaus.org=127.0.0.[4..7]*6 + zen.spamhaus.org=127.0.0.3*4 + zen.spamhaus.org=127.0.0.2*3 + +# User Overrides +myhostname = mail.nasarek.dev + diff --git a/mailcow/data/conf/postfix/master.cf b/mailcow/data/conf/postfix/master.cf new file mode 100644 index 0000000..d5114df --- /dev/null +++ b/mailcow/data/conf/postfix/master.cf @@ -0,0 +1,146 @@ +# inter-mx with postscreen on 25/tcp +smtp inet n - n - 1 postscreen +10025 inet n - n - 1 postscreen + -o postscreen_upstream_proxy_protocol=haproxy + -o syslog_name=haproxy +smtpd pass - - n - - smtpd + -o smtpd_sasl_auth_enable=no + -o smtpd_sender_restrictions=permit_mynetworks,reject_unlisted_sender,reject_unknown_sender_domain + +# smtpd tls-wrapped (smtps) on 465/tcp +# TLS protocol can be modified by setting smtps_smtpd_tls_mandatory_protocols in extra.cf +smtps inet n - n - - smtpd + -o smtpd_tls_wrappermode=yes + -o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject + -o smtpd_tls_mandatory_protocols=$smtps_smtpd_tls_mandatory_protocols + -o tls_preempt_cipherlist=yes + -o cleanup_service_name=smtp_sender_cleanup + -o syslog_name=postfix/smtps +10465 inet n - n - - smtpd + -o smtpd_upstream_proxy_protocol=haproxy + -o smtpd_tls_wrappermode=yes + -o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject + -o smtpd_tls_mandatory_protocols=$smtps_smtpd_tls_mandatory_protocols + -o tls_preempt_cipherlist=yes + -o cleanup_service_name=smtp_sender_cleanup + -o syslog_name=postfix/smtps-haproxy + +# smtpd with starttls on 587/tcp +# TLS protocol can be modified by setting submission_smtpd_tls_mandatory_protocols in extra.cf +submission inet n - n - - smtpd + -o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject + -o smtpd_enforce_tls=yes + -o smtpd_tls_security_level=encrypt + -o smtpd_tls_mandatory_protocols=$submission_smtpd_tls_mandatory_protocols + -o tls_preempt_cipherlist=yes + -o cleanup_service_name=smtp_sender_cleanup + -o syslog_name=postfix/submission +10587 inet n - n - - smtpd + -o smtpd_upstream_proxy_protocol=haproxy + -o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject + -o smtpd_enforce_tls=yes + -o smtpd_tls_security_level=encrypt + -o smtpd_tls_mandatory_protocols=$submission_smtpd_tls_mandatory_protocols + -o tls_preempt_cipherlist=yes + -o cleanup_service_name=smtp_sender_cleanup + -o syslog_name=postfix/submission-haproxy + +# used by SOGo +# smtpd_sender_restrictions should match main.cf, but with check_sasl_access prepended for login-as-mailbox-user function +588 inet n - n - - smtpd + -o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject + -o smtpd_tls_auth_only=no + -o smtpd_sender_restrictions=check_sasl_access,regexp:/opt/postfix/conf/allow_mailcow_local.regexp,reject_authenticated_sender_login_mismatch,permit_mynetworks,permit_sasl_authenticated,reject_unlisted_sender,reject_unknown_sender_domain + -o cleanup_service_name=smtp_sender_cleanup + -o syslog_name=postfix/sogo + +# used to reinject quarantine mails +590 inet n - n - - smtpd + -o smtpd_helo_restrictions= + -o smtpd_client_restrictions=permit_mynetworks,reject + -o smtpd_tls_auth_only=no + -o smtpd_milters= + -o non_smtpd_milters= + -o syslog_name=postfix/quarantine + +# used to send bcc mails +591 inet n - n - - smtpd + -o smtpd_helo_restrictions= + -o smtpd_client_restrictions=permit_mynetworks,reject + -o smtpd_tls_auth_only=no + -o smtpd_milters= + -o non_smtpd_milters= + -o syslog_name=postfix/bcc + +# enforced smtp connector +smtp_enforced_tls unix - - n - - smtp + -o smtp_tls_security_level=encrypt + -o syslog_name=enforced-tls-smtp + -o smtp_delivery_status_filter=pcre:/opt/postfix/conf/smtp_dsn_filter + +# smtp connector used, when a transport map matched +# this helps to have different sasl maps than we have with sender dependent transport maps +smtp_via_transport_maps unix - - n - - smtp + -o smtp_sasl_password_maps=proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_transport_maps.cf + +tlsproxy unix - - n - 0 tlsproxy +dnsblog unix - - n - 0 dnsblog +pickup fifo n - n 60 1 pickup +cleanup unix n - n - 0 cleanup +qmgr fifo n - n 300 1 qmgr +tlsmgr unix - - n 1000? 1 tlsmgr +rewrite unix - - n - - trivial-rewrite +bounce unix - - n - 0 bounce +defer unix - - n - 0 bounce +trace unix - - n - 0 bounce +verify unix - - n - 1 verify +flush unix n - n 1000? 0 flush +proxymap unix - - n - - proxymap +proxywrite unix - - n - 1 proxymap +smtp unix - - n - - smtp +relay unix - - n - - smtp +showq unix n - n - - showq +error unix - - n - - error +retry unix - - n - - error +discard unix - - n - - discard +local unix - n n - - local +virtual unix - n n - - virtual +lmtp unix - - n - - lmtp flags=O +anvil unix - - n - 1 anvil +scache unix - - n - 1 scache +maildrop unix - n n - - pipe flags=DRhu + user=vmail argv=/usr/bin/maildrop -d ${recipient} + +# used to anonymize sender IP +smtp_sender_cleanup unix n - y - 0 cleanup + -o header_checks=$smtp_header_checks + +# start whitelist_fwd +127.0.0.1:10027 inet n n n - 0 spawn user=nobody argv=/usr/local/bin/whitelist_forwardinghosts.sh +# end whitelist_fwd + +# start watchdog-specific +# logs to local7 (hidden) +589 inet n - n - - smtpd + -o smtpd_client_restrictions=permit_mynetworks,reject + -o syslog_name=watchdog + -o syslog_facility=local7 + -o smtpd_milters= + -o cleanup_service_name=watchdog_cleanup + -o non_smtpd_milters= +watchdog_cleanup unix n - n - 0 cleanup + -o syslog_name=watchdog + -o syslog_facility=local7 + -o queue_service_name=watchdog_qmgr +watchdog_qmgr fifo n - n 300 1 qmgr + -o syslog_facility=local7 + -o syslog_name=watchdog + -o rewrite_service_name=watchdog_rewrite +watchdog_rewrite unix - - n - - trivial-rewrite + -o syslog_facility=local7 + -o syslog_name=watchdog + -o local_transport=watchdog_discard +watchdog_discard unix - - n - - discard + -o syslog_facility=local7 + -o syslog_name=watchdog +# end watchdog-specific diff --git a/mailcow/data/conf/postfix/postscreen_access.cidr b/mailcow/data/conf/postfix/postscreen_access.cidr new file mode 100644 index 0000000..c6ed2be --- /dev/null +++ b/mailcow/data/conf/postfix/postscreen_access.cidr @@ -0,0 +1,2004 @@ +# Whitelist generated by Postwhite v3.4 on Sat Mar 1 00:19:29 UTC 2025 +# https://github.com/stevejenkins/postwhite/ +# 2000 total rules +2a00:1450:4000::/36 permit +2a01:111:f400::/48 permit +2a01:111:f403:8000::/50 permit +2a01:111:f403:8000::/51 permit +2a01:111:f403::/49 permit +2a01:111:f403:c000::/51 permit +2a01:111:f403:f000::/52 permit +2a01:b747:3000:200::/56 permit +2a01:b747:3001:200::/56 permit +2a01:b747:3002:200::/56 permit +2a01:b747:3003:200::/56 permit +2a01:b747:3004:200::/56 permit +2a01:b747:3005:200::/56 permit +2a01:b747:3006:200::/56 permit +2a02:a60:0:5::/64 permit +2c0f:fb50:4000::/36 permit +2.207.151.53 permit +3.70.123.177 permit +3.93.157.0/24 permit +3.94.40.108 permit +3.129.120.190 permit +3.210.190.0/24 permit +8.20.114.31 permit +8.25.194.0/23 permit +8.25.196.0/23 permit +12.130.86.238 permit +13.110.208.0/21 permit +13.110.209.0/24 permit +13.110.216.0/22 permit +13.110.224.0/20 permit +13.111.0.0/16 permit +13.111.191.0/24 permit +15.200.21.50 permit +15.200.44.248 permit +15.200.201.185 permit +17.41.0.0/16 permit +17.57.155.0/24 permit +17.57.156.0/24 permit +17.58.0.0/16 permit +17.142.0.0/15 permit +18.97.0.8/30 permit +18.97.1.184/29 permit +18.97.2.64/26 permit +18.156.89.250 permit +18.157.243.190 permit +18.194.95.56 permit +18.198.96.88 permit +18.208.124.128/25 permit +18.216.232.154 permit +18.235.27.253 permit +18.236.40.242 permit +18.236.56.161 permit +20.51.6.32/30 permit +20.51.98.61 permit +20.52.52.2 permit +20.52.128.133 permit +20.59.80.4/30 permit +20.63.210.192/28 permit +20.69.8.108/30 permit +20.70.246.20 permit +20.76.201.171 permit +20.83.222.104/30 permit +20.88.157.184/30 permit +20.94.180.64/28 permit +20.97.34.220/30 permit +20.98.148.156/30 permit +20.98.194.68/30 permit +20.105.209.76/30 permit +20.107.239.64/30 permit +20.112.250.133 permit +20.118.139.208/30 permit +20.141.10.196 permit +20.185.214.0/27 permit +20.185.214.32/27 permit +20.185.214.64/27 permit +20.231.239.246 permit +20.236.44.162 permit +23.103.224.0/19 permit +23.249.208.0/20 permit +23.251.224.0/19 permit +23.253.141.0/24 permit +23.253.182.0/23 permit +23.253.182.103 permit +23.253.183.145 permit +23.253.183.146 permit +23.253.183.147 permit +23.253.183.148 permit +23.253.183.150 permit +27.123.204.128/30 permit +27.123.204.132/31 permit +27.123.204.148/30 permit +27.123.204.152 permit +27.123.204.168/30 permit +27.123.204.172 permit +27.123.204.188/30 permit +27.123.204.192 permit +27.123.206.50/31 permit +27.123.206.56/29 permit +27.123.206.76/30 permit +27.123.206.80/28 permit +31.25.48.222 permit +31.47.251.17 permit +34.195.217.107 permit +34.212.163.75 permit +34.215.104.144 permit +34.218.116.3 permit +34.225.212.172 permit +35.161.32.253 permit +35.167.93.243 permit +35.176.132.251 permit +35.190.247.0/24 permit +35.191.0.0/16 permit +35.205.92.9 permit +35.242.169.159 permit +37.188.97.188 permit +37.218.248.47 permit +37.218.249.47 permit +37.218.251.62 permit +39.156.163.64/29 permit +40.92.0.0/15 permit +40.92.0.0/16 permit +40.107.0.0/16 permit +40.112.65.63 permit +40.233.64.216 permit +40.233.83.78 permit +40.233.88.28 permit +44.206.138.57 permit +44.217.45.156 permit +44.236.56.93 permit +44.238.220.251 permit +45.14.148.0/22 permit +46.19.170.16 permit +46.226.48.0/21 permit +46.228.36.37 permit +46.228.36.38/31 permit +46.228.36.212/30 permit +46.228.36.216/31 permit +46.228.37.1 permit +46.228.37.2 permit +46.228.37.3 permit +46.228.37.4 permit +46.228.37.5 permit +46.228.37.6/31 permit +46.228.37.8/31 permit +46.228.37.10/31 permit +46.228.37.12/30 permit +46.228.37.16/30 permit +46.228.37.20/30 permit +46.228.37.24/30 permit +46.228.37.28/31 permit +46.228.37.30/31 permit +46.228.37.32/29 permit +46.228.37.40/29 permit +46.228.37.48/31 permit +46.228.37.50/31 permit +46.228.37.52/30 permit +46.228.37.56/31 permit +46.228.38.33 permit +46.228.38.34/31 permit +46.228.38.36/31 permit +46.228.38.38 permit +46.228.38.53 permit +46.228.38.54/31 permit +46.228.38.56/31 permit +46.228.38.58 permit +46.228.38.101 permit +46.228.38.102/31 permit +46.228.38.128/28 permit +46.228.38.144/29 permit +46.228.38.152/31 permit +46.228.38.154 permit +46.228.39.64/27 permit +46.228.39.96/30 permit +46.228.39.100/30 permit +46.228.39.104/29 permit +46.228.39.112/31 permit +46.243.88.174 permit +46.243.88.175 permit +46.243.88.176 permit +46.243.88.177 permit +46.243.95.179 permit +46.243.95.180 permit +50.18.45.249 permit +50.18.121.236 permit +50.18.121.248 permit +50.18.123.221 permit +50.18.124.70 permit +50.18.125.97 permit +50.18.125.237 permit +50.18.126.162 permit +50.31.32.0/19 permit +50.31.36.205 permit +50.56.130.220 permit +50.56.130.221 permit +50.56.130.222 permit +52.1.14.157 permit +52.5.230.59 permit +52.27.5.72 permit +52.27.28.47 permit +52.28.63.81 permit +52.36.138.31 permit +52.37.142.146 permit +52.50.24.208 permit +52.58.216.183 permit +52.59.143.3 permit +52.60.41.5 permit +52.60.115.116 permit +52.61.91.9 permit +52.71.0.205 permit +52.94.124.0/28 permit +52.95.48.152/29 permit +52.95.49.88/29 permit +52.96.91.34 permit +52.96.111.82 permit +52.96.172.98 permit +52.96.214.50 permit +52.96.222.194 permit +52.96.222.226 permit +52.96.223.2 permit +52.96.228.130 permit +52.96.229.242 permit +52.100.0.0/15 permit +52.102.0.0/16 permit +52.103.0.0/17 permit +52.119.213.144/28 permit +52.185.106.240/28 permit +52.200.59.0/24 permit +52.207.191.216 permit +52.222.62.51 permit +52.222.73.83 permit +52.222.73.120 permit +52.222.75.85 permit +52.222.89.228 permit +52.234.172.96/28 permit +52.235.253.128 permit +52.236.28.240/28 permit +54.90.148.255 permit +54.165.19.38 permit +54.174.52.0/24 permit +54.174.57.0/24 permit +54.174.59.0/24 permit +54.174.60.0/23 permit +54.174.63.0/24 permit +54.186.193.102 permit +54.191.223.56 permit +54.213.20.246 permit +54.214.39.184 permit +54.240.0.0/18 permit +54.240.64.0/19 permit +54.240.96.0/19 permit +54.241.16.209 permit +54.244.54.130 permit +54.244.242.0/24 permit +54.255.61.23 permit +57.103.64.0/18 permit +62.13.128.0/24 permit +62.13.129.128/25 permit +62.13.136.0/21 permit +62.13.144.0/21 permit +62.13.152.0/21 permit +62.17.146.128/26 permit +62.179.121.0/24 permit +62.201.172.0/27 permit +62.201.172.32/27 permit +62.253.227.114 permit +63.80.14.0/23 permit +63.128.21.0/24 permit +63.143.57.128/25 permit +63.143.59.128/25 permit +64.18.0.0/20 permit +64.20.241.45 permit +64.69.212.0/24 permit +64.71.149.160/28 permit +64.79.155.0/24 permit +64.79.155.192 permit +64.79.155.193 permit +64.79.155.205 permit +64.79.155.206 permit +64.127.115.252 permit +64.132.88.0/23 permit +64.132.92.0/24 permit +64.207.219.7 permit +64.207.219.8 permit +64.207.219.9 permit +64.207.219.10 permit +64.207.219.11 permit +64.207.219.12 permit +64.207.219.13 permit +64.207.219.14 permit +64.207.219.15 permit +64.207.219.24 permit +64.207.219.25 permit +64.207.219.26 permit +64.207.219.71 permit +64.207.219.72 permit +64.207.219.73 permit +64.207.219.74 permit +64.207.219.75 permit +64.207.219.76 permit +64.207.219.77 permit +64.207.219.78 permit +64.207.219.79 permit +64.207.219.88 permit +64.207.219.89 permit +64.207.219.90 permit +64.207.219.135 permit +64.207.219.136 permit +64.207.219.137 permit +64.207.219.138 permit +64.207.219.139 permit +64.207.219.140 permit +64.207.219.141 permit +64.207.219.142 permit +64.207.219.143 permit +64.233.160.0/19 permit +65.52.80.137 permit +65.54.51.64/26 permit +65.54.61.64/26 permit +65.54.121.120/29 permit +65.54.190.0/24 permit +65.54.241.0/24 permit +65.55.29.77 permit +65.55.33.64/28 permit +65.55.34.0/24 permit +65.55.42.224/28 permit +65.55.52.224/27 permit +65.55.78.128/25 permit +65.55.81.48/28 permit +65.55.90.0/24 permit +65.55.94.0/25 permit +65.55.111.0/24 permit +65.55.113.64/26 permit +65.55.116.0/25 permit +65.55.126.0/25 permit +65.55.174.0/25 permit +65.55.178.128/27 permit +65.55.234.192/26 permit +65.110.161.77 permit +65.123.29.213 permit +65.123.29.220 permit +65.212.180.36 permit +66.102.0.0/20 permit +66.119.150.192/26 permit +66.162.193.226/31 permit +66.163.184.0/24 permit +66.163.185.0/24 permit +66.163.186.0/24 permit +66.163.187.0/24 permit +66.163.188.0/24 permit +66.163.189.0/24 permit +66.163.190.0/24 permit +66.163.191.0/24 permit +66.170.126.97 permit +66.196.80.112/28 permit +66.196.80.144/29 permit +66.196.80.193 permit +66.196.81.104/29 permit +66.196.81.112/29 permit +66.196.81.120 permit +66.196.81.125 permit +66.196.81.126/31 permit +66.196.81.128/28 permit +66.196.81.144/29 permit +66.196.81.152/31 permit +66.196.81.154 permit +66.196.81.225 permit +66.196.81.226/31 permit +66.196.81.228/30 permit +66.196.81.232/31 permit +66.196.81.234 permit +66.211.170.88/29 permit +66.211.184.0/23 permit +66.218.74.64/30 permit +66.218.74.68/31 permit +66.218.75.112/30 permit +66.218.75.116/31 permit +66.218.75.144/31 permit +66.218.75.146 permit +66.218.75.152 permit +66.218.75.192/30 permit +66.218.75.196/31 permit +66.218.75.203 permit +66.218.75.212/30 permit +66.218.75.216/31 permit +66.218.75.223 permit +66.218.75.232/30 permit +66.218.75.236/31 permit +66.218.75.243 permit +66.218.75.252/31 permit +66.218.75.254 permit +66.220.144.128/25 permit +66.220.155.0/24 permit +66.220.157.0/25 permit +66.231.80.0/20 permit +66.240.227.0/24 permit +66.249.80.0/20 permit +67.23.31.6 permit +67.72.99.26 permit +67.195.22.113 permit +67.195.22.116/30 permit +67.195.23.144/30 permit +67.195.23.148 permit +67.195.60.45 permit +67.195.60.46/31 permit +67.195.60.48/31 permit +67.195.60.50 permit +67.195.60.146 permit +67.195.60.155 permit +67.195.60.156 permit +67.195.87.64 permit +67.195.87.81 permit +67.195.87.82/31 permit +67.195.87.84/31 permit +67.195.87.86 permit +67.195.87.128 permit +67.195.87.145 permit +67.195.87.146/31 permit +67.195.87.192 permit +67.195.87.209 permit +67.195.87.210/31 permit +67.195.124.137 permit +67.195.124.139 permit +67.195.124.141 permit +67.195.124.143 permit +67.195.124.145 permit +67.195.124.147 permit +67.219.240.0/20 permit +67.221.168.65 permit +67.228.2.24/30 permit +67.228.21.184/29 permit +67.228.37.4/30 permit +67.231.145.42 permit +67.231.153.30 permit +68.142.230.64/31 permit +68.142.230.69 permit +68.142.230.70/31 permit +68.142.230.72/30 permit +68.142.230.76/31 permit +68.142.230.78 permit +68.232.140.138 permit +68.232.157.143 permit +68.232.192.0/20 permit +69.63.178.128/25 permit +69.63.181.0/24 permit +69.63.184.0/25 permit +69.65.42.195 permit +69.65.49.192/29 permit +69.72.32.0/20 permit +69.72.40.93 permit +69.72.40.94/31 permit +69.72.40.96/30 permit +69.72.47.205 permit +69.147.84.227 permit +69.162.98.0/24 permit +69.169.224.0/20 permit +69.171.232.0/24 permit +69.171.244.0/23 permit +70.37.151.128/25 permit +70.42.149.35 permit +72.3.185.0/24 permit +72.14.192.0/18 permit +72.21.192.0/19 permit +72.21.217.142 permit +72.30.234.152/29 permit +72.30.236.160/30 permit +72.30.236.164/31 permit +72.30.236.180/30 permit +72.30.236.184/31 permit +72.30.236.208/30 permit +72.30.236.212/31 permit +72.30.236.224/30 permit +72.30.236.228/31 permit +72.30.236.244/30 permit +72.30.236.248/31 permit +72.30.237.32/30 permit +72.30.237.36/31 permit +72.30.237.52/30 permit +72.30.237.56/31 permit +72.30.237.80/31 permit +72.30.237.96/30 permit +72.30.237.100/31 permit +72.30.237.116/30 permit +72.30.237.120/31 permit +72.30.237.144/30 permit +72.30.237.148/31 permit +72.30.237.160/30 permit +72.30.237.164/31 permit +72.30.237.180/30 permit +72.30.237.184/31 permit +72.30.237.204/30 permit +72.30.238.116/30 permit +72.30.238.120/31 permit +72.30.238.128 permit +72.30.238.133 permit +72.30.238.168/30 permit +72.30.238.172/31 permit +72.30.238.188/30 permit +72.30.238.192 permit +72.30.238.197 permit +72.30.238.224/31 permit +72.30.238.240/30 permit +72.30.238.244/31 permit +72.30.239.5 permit +72.30.239.37 permit +72.30.239.38/31 permit +72.30.239.40/31 permit +72.30.239.42 permit +72.30.239.57 permit +72.30.239.64 permit +72.30.239.69 permit +72.30.239.89 permit +72.30.239.90/31 permit +72.30.239.92/31 permit +72.30.239.94 permit +72.30.239.128 permit +72.30.239.198 permit +72.30.239.224/30 permit +72.30.239.228/31 permit +72.30.239.244/30 permit +72.30.239.248/31 permit +72.32.154.0/24 permit +72.32.217.0/24 permit +72.32.243.0/24 permit +72.52.72.32/28 permit +74.6.128.0/24 permit +74.6.129.0/24 permit +74.6.130.0/24 permit +74.6.131.0/24 permit +74.6.132.0/24 permit +74.6.133.0/24 permit +74.6.134.0/24 permit +74.6.135.0/24 permit +74.63.212.0/24 permit +74.63.234.75 permit +74.63.236.0/24 permit +74.86.113.28/30 permit +74.86.129.240/30 permit +74.86.131.208/30 permit +74.86.132.208/30 permit +74.86.160.160/30 permit +74.86.164.188/30 permit +74.86.171.192/30 permit +74.86.195.28/30 permit +74.86.207.36/30 permit +74.86.226.216/30 permit +74.86.236.240/30 permit +74.86.241.250/31 permit +74.112.67.243 permit +74.125.0.0/16 permit +74.202.227.40 permit +74.208.4.200 permit +74.208.4.201 permit +74.208.4.220 permit +74.208.4.221 permit +74.209.250.0/24 permit +75.2.70.75 permit +76.223.128.0/19 permit +76.223.176.0/20 permit +77.238.176.0/24 permit +77.238.177.0/24 permit +77.238.178.0/24 permit +77.238.179.0/24 permit +77.238.189.21 permit +77.238.189.23 permit +77.238.189.35 permit +77.238.189.39 permit +77.238.189.58/31 permit +77.238.189.60/30 permit +77.238.189.64/29 permit +77.238.189.76/31 permit +77.238.189.128/30 permit +77.238.189.132/31 permit +77.238.189.137 permit +77.238.189.138/31 permit +77.238.189.140/31 permit +77.238.189.142 permit +77.238.189.146/31 permit +77.238.189.148/30 permit +81.223.46.0/27 permit +82.165.159.2 permit +82.165.159.3 permit +82.165.159.4 permit +82.165.159.12 permit +82.165.159.13 permit +82.165.159.14 permit +82.165.159.34 permit +82.165.159.35 permit +82.165.159.40 permit +82.165.159.41 permit +82.165.159.42 permit +82.165.159.45 permit +82.165.159.130 permit +82.165.159.131 permit +84.116.6.0/23 permit +84.116.36.0/24 permit +84.116.50.0/23 permit +85.158.136.0/21 permit +86.61.88.25 permit +87.238.80.0/21 permit +87.248.103.12 permit +87.248.103.21 permit +87.248.103.23 permit +87.248.103.27 permit +87.248.103.29 permit +87.248.103.36 permit +87.248.103.42/31 permit +87.248.103.44 permit +87.248.103.64 permit +87.248.103.76 permit +87.248.103.83 permit +87.248.103.92/31 permit +87.248.103.94 permit +87.248.103.107 permit +87.248.103.113 permit +87.248.103.122 permit +87.248.110.0/24 permit +87.248.117.30 permit +87.248.117.65 permit +87.248.117.67 permit +87.248.117.70 permit +87.248.117.73 permit +87.248.117.74 permit +87.248.117.86 permit +87.248.117.97 permit +87.248.117.98 permit +87.248.117.104 permit +87.248.117.119 permit +87.248.117.124 permit +87.248.117.185 permit +87.248.117.196 permit +87.248.117.198 permit +87.248.117.201 permit +87.248.117.202 permit +87.248.117.205 permit +87.253.232.0/21 permit +89.22.108.0/24 permit +91.211.240.0/22 permit +94.169.2.0/23 permit +94.236.119.0/26 permit +94.245.112.0/27 permit +94.245.112.10/31 permit +95.131.104.0/21 permit +96.43.144.0/20 permit +96.43.144.64/28 permit +96.43.144.64/31 permit +96.43.148.64/28 permit +96.43.148.64/31 permit +96.43.151.64/28 permit +98.97.248.0/21 permit +98.136.44.181 permit +98.136.44.182/31 permit +98.136.44.184 permit +98.136.164.36/31 permit +98.136.164.64/29 permit +98.136.164.72/30 permit +98.136.164.76/31 permit +98.136.164.78 permit +98.136.172.32/30 permit +98.136.172.36/31 permit +98.136.185.29 permit +98.136.185.42/31 permit +98.136.185.46 permit +98.136.185.115 permit +98.136.214.28/30 permit +98.136.214.97 permit +98.136.214.98/31 permit +98.136.214.100 permit +98.136.214.116/30 permit +98.136.214.120/31 permit +98.136.214.157 permit +98.136.214.158/31 permit +98.136.214.160/31 permit +98.136.214.162 permit +98.136.214.190/31 permit +98.136.214.192/30 permit +98.136.214.238/31 permit +98.136.214.255 permit +98.136.215.0/31 permit +98.136.215.2 permit +98.136.215.3 permit +98.136.215.4/31 permit +98.136.215.6 permit +98.136.215.114/31 permit +98.136.215.116/31 permit +98.136.215.118 permit +98.136.215.136/30 permit +98.136.215.148/30 permit +98.136.215.152/31 permit +98.136.215.179 permit +98.136.215.180/30 permit +98.136.215.184 permit +98.136.215.208/30 permit +98.136.215.212/31 permit +98.136.217.1 permit +98.136.217.2 permit +98.136.217.3 permit +98.136.217.4/30 permit +98.136.217.8/31 permit +98.136.217.10/31 permit +98.136.217.12/30 permit +98.136.217.16/30 permit +98.136.217.20/30 permit +98.136.218.39 permit +98.136.218.40/29 permit +98.136.218.48/28 permit +98.136.218.67 permit +98.136.218.68/30 permit +98.136.218.72/30 permit +98.137.12.48/30 permit +98.137.12.52/31 permit +98.137.12.54 permit +98.137.12.177 permit +98.137.12.178/31 permit +98.137.12.180/31 permit +98.137.12.182 permit +98.137.12.192/27 permit +98.137.12.224/28 permit +98.137.12.240/29 permit +98.137.12.248/30 permit +98.137.12.252/31 permit +98.137.12.254 permit +98.137.13.81 permit +98.137.13.82 permit +98.137.13.87 permit +98.137.13.88 permit +98.137.13.91 permit +98.137.13.92 permit +98.137.13.97 permit +98.137.13.98 permit +98.137.13.101 permit +98.137.13.102 permit +98.137.13.107 permit +98.137.13.108 permit +98.137.13.111 permit +98.137.13.112 permit +98.137.13.117 permit +98.137.13.118 permit +98.137.13.121 permit +98.137.13.122 permit +98.137.13.127 permit +98.137.13.128 permit +98.137.13.131 permit +98.137.13.132 permit +98.137.13.137 permit +98.137.13.138 permit +98.137.64.0/24 permit +98.137.65.0/24 permit +98.137.66.0/24 permit +98.137.67.0/24 permit +98.137.68.0/24 permit +98.137.69.0/24 permit +98.137.70.0/24 permit +98.137.71.0/24 permit +98.137.176.58/31 permit +98.137.177.247 permit +98.138.31.128/30 permit +98.138.31.132/31 permit +98.138.31.192/30 permit +98.138.31.196/31 permit +98.138.31.202/31 permit +98.138.31.204/30 permit +98.138.31.232/30 permit +98.138.31.236/31 permit +98.138.82.208/30 permit +98.138.82.212/31 permit +98.138.83.176/31 permit +98.138.83.179 permit +98.138.83.180/31 permit +98.138.84.37 permit +98.138.84.38/31 permit +98.138.84.40/29 permit +98.138.85.128/30 permit +98.138.85.132/31 permit +98.138.85.148/30 permit +98.138.85.152/31 permit +98.138.85.168/30 permit +98.138.85.172/31 permit +98.138.85.188/30 permit +98.138.85.192/31 permit +98.138.85.208/30 permit +98.138.85.212/31 permit +98.138.85.228/30 permit +98.138.85.232/31 permit +98.138.85.248/30 permit +98.138.85.252/31 permit +98.138.86.69 permit +98.138.86.156/31 permit +98.138.86.192/30 permit +98.138.86.196/31 permit +98.138.87.1 permit +98.138.87.2/31 permit +98.138.87.4/31 permit +98.138.87.6 permit +98.138.87.7 permit +98.138.87.8/31 permit +98.138.87.10/31 permit +98.138.87.12 permit +98.138.87.16/30 permit +98.138.87.64/30 permit +98.138.87.68/31 permit +98.138.87.73 permit +98.138.87.74/31 permit +98.138.87.76/31 permit +98.138.87.78 permit +98.138.87.144/30 permit +98.138.87.148/31 permit +98.138.87.192/30 permit +98.138.87.196/31 permit +98.138.88.105 permit +98.138.88.106 permit +98.138.88.128/30 permit +98.138.88.132/31 permit +98.138.88.148/30 permit +98.138.88.152/31 permit +98.138.88.232/29 permit +98.138.89.160/28 permit +98.138.89.192/29 permit +98.138.89.232/31 permit +98.138.89.234 permit +98.138.89.240 permit +98.138.89.244/31 permit +98.138.89.246 permit +98.138.89.248/30 permit +98.138.89.252/31 permit +98.138.89.254 permit +98.138.90.64/28 permit +98.138.90.80/29 permit +98.138.90.88/30 permit +98.138.90.92/31 permit +98.138.90.95 permit +98.138.90.96/30 permit +98.138.90.100 permit +98.138.90.104/30 permit +98.138.90.108/31 permit +98.138.90.113 permit +98.138.90.114/31 permit +98.138.90.116/31 permit +98.138.90.118 permit +98.138.90.122/31 permit +98.138.90.124/30 permit +98.138.90.131 permit +98.138.90.132/30 permit +98.138.90.136 permit +98.138.91.1 permit +98.138.91.2/31 permit +98.138.91.4/31 permit +98.138.91.6 permit +98.138.100.220/30 permit +98.138.100.224/30 permit +98.138.100.228/31 permit +98.138.101.160/28 permit +98.138.101.176/29 permit +98.138.104.96/30 permit +98.138.104.100 permit +98.138.104.112/30 permit +98.138.104.116 permit +98.138.120.36/30 permit +98.138.120.48/28 permit +98.138.197.46/31 permit +98.138.197.48/30 permit +98.138.197.169 permit +98.138.197.176 permit +98.138.197.180 permit +98.138.198.52/31 permit +98.138.198.54 permit +98.138.198.56 permit +98.138.198.61 permit +98.138.198.190/31 permit +98.138.198.239 permit +98.138.199.6 permit +98.138.199.9 permit +98.138.199.48 permit +98.138.199.83 permit +98.138.199.84 permit +98.138.199.86/31 permit +98.138.199.94 permit +98.138.199.116 permit +98.138.199.208/30 permit +98.138.199.212/31 permit +98.138.199.218/31 permit +98.138.199.220/30 permit +98.138.199.236 permit +98.138.206.67 permit +98.138.206.73 permit +98.138.206.143 permit +98.138.206.167 permit +98.138.206.169 permit +98.138.206.173 permit +98.138.207.9 permit +98.138.207.10/31 permit +98.138.207.12/31 permit +98.138.207.14 permit +98.138.210.39 permit +98.138.210.64/30 permit +98.138.210.68/31 permit +98.138.210.74/31 permit +98.138.210.76/30 permit +98.138.210.84 permit +98.138.210.86 permit +98.138.210.89 permit +98.138.210.93 permit +98.138.210.94 permit +98.138.210.96 permit +98.138.210.98 permit +98.138.210.101 permit +98.138.210.111 permit +98.138.210.115 permit +98.138.210.122 permit +98.138.210.136 permit +98.138.210.146 permit +98.138.210.148 permit +98.138.210.155 permit +98.138.210.158/31 permit +98.138.210.166 permit +98.138.210.169 permit +98.138.210.175 permit +98.138.210.177 permit +98.138.210.181 permit +98.138.210.182/31 permit +98.138.210.187 permit +98.138.210.189 permit +98.138.210.201 permit +98.138.210.203 permit +98.138.210.210 permit +98.138.210.240/30 permit +98.138.210.244/31 permit +98.138.211.122/31 permit +98.138.211.124/30 permit +98.138.211.128/30 permit +98.138.211.132/31 permit +98.138.211.148/30 permit +98.138.211.152/31 permit +98.138.211.216/30 permit +98.138.211.220/31 permit +98.138.213.128/30 permit +98.138.213.132/31 permit +98.138.213.138/31 permit +98.138.213.140/30 permit +98.138.213.148/30 permit +98.138.213.152/31 permit +98.138.213.158/31 permit +98.138.213.160/30 permit +98.138.213.168/30 permit +98.138.213.172/31 permit +98.138.213.237 permit +98.138.213.238/31 permit +98.138.213.240/31 permit +98.138.213.242 permit +98.138.215.12/30 permit +98.138.215.16/28 permit +98.138.217.216/30 permit +98.138.217.220/31 permit +98.138.226.30/31 permit +98.138.226.56/29 permit +98.138.226.64/30 permit +98.138.226.68/31 permit +98.138.226.74/31 permit +98.138.226.76/30 permit +98.138.226.84/30 permit +98.138.226.88/31 permit +98.138.226.124/30 permit +98.138.226.128/30 permit +98.138.226.132/31 permit +98.138.226.160/29 permit +98.138.226.168/31 permit +98.138.226.240/30 permit +98.138.226.244 permit +98.138.227.32/30 permit +98.138.227.36/31 permit +98.138.227.56/30 permit +98.138.227.60/31 permit +98.138.227.80/30 permit +98.138.227.84/31 permit +98.138.227.104/30 permit +98.138.227.108/31 permit +98.138.227.128/30 permit +98.138.227.132/31 permit +98.138.229.24/29 permit +98.138.229.32/31 permit +98.138.229.122/31 permit +98.138.229.138/31 permit +98.138.229.154/31 permit +98.138.229.170/31 permit +98.139.164.96/30 permit +98.139.164.100/30 permit +98.139.164.104/29 permit +98.139.164.112/30 permit +98.139.172.112/30 permit +98.139.172.116/31 permit +98.139.175.65 permit +98.139.175.66/31 permit +98.139.175.68/30 permit +98.139.175.72/29 permit +98.139.175.80/29 permit +98.139.175.88/30 permit +98.139.175.92/31 permit +98.139.175.94 permit +98.139.210.112/30 permit +98.139.210.128/30 permit +98.139.210.132/31 permit +98.139.210.138/31 permit +98.139.210.140/30 permit +98.139.210.148/30 permit +98.139.210.152/31 permit +98.139.210.170/31 permit +98.139.210.172/30 permit +98.139.210.180/30 permit +98.139.210.184/31 permit +98.139.210.190/31 permit +98.139.210.192/30 permit +98.139.210.196/31 permit +98.139.210.202/31 permit +98.139.210.204/30 permit +98.139.211.160/30 permit +98.139.211.192/28 permit +98.139.212.160/28 permit +98.139.212.176/29 permit +98.139.212.184/30 permit +98.139.212.188/31 permit +98.139.212.192/27 permit +98.139.212.224/28 permit +98.139.212.240/29 permit +98.139.212.248/30 permit +98.139.213.8/31 permit +98.139.213.10/31 permit +98.139.213.12/30 permit +98.139.214.155 permit +98.139.214.156/30 permit +98.139.214.221 permit +98.139.215.228/31 permit +98.139.215.230 permit +98.139.215.248/30 permit +98.139.215.252/31 permit +98.139.215.254 permit +98.139.218.53 permit +98.139.218.61 permit +98.139.218.63 permit +98.139.219.128 permit +98.139.219.138 permit +98.139.219.159 permit +98.139.219.177 permit +98.139.219.184 permit +98.139.219.193 permit +98.139.219.194 permit +98.139.219.196 permit +98.139.219.203 permit +98.139.219.209 permit +98.139.219.215 permit +98.139.219.216/31 permit +98.139.219.231 permit +98.139.220.43 permit +98.139.220.50 permit +98.139.220.57 permit +98.139.220.63 permit +98.139.220.71 permit +98.139.220.73 permit +98.139.220.85 permit +98.139.220.96 permit +98.139.220.100 permit +98.139.220.103 permit +98.139.220.104 permit +98.139.220.106/31 permit +98.139.220.110/31 permit +98.139.220.112/31 permit +98.139.220.144 permit +98.139.220.152 permit +98.139.220.157 permit +98.139.220.158 permit +98.139.220.163 permit +98.139.220.164 permit +98.139.220.170 permit +98.139.220.182 permit +98.139.220.187 permit +98.139.220.189 permit +98.139.220.193 permit +98.139.220.198 permit +98.139.220.203 permit +98.139.220.208/31 permit +98.139.220.212 permit +98.139.220.214/31 permit +98.139.220.219 permit +98.139.220.223 permit +98.139.220.232 permit +98.139.220.238 permit +98.139.220.243 permit +98.139.220.245 permit +98.139.220.253 permit +98.139.221.43 permit +98.139.221.60/30 permit +98.139.221.156/30 permit +98.139.221.232/30 permit +98.139.221.236/31 permit +98.139.221.250 permit +98.139.244.47 permit +98.139.244.49 permit +98.139.244.50/31 permit +98.139.244.52/30 permit +98.139.244.57 permit +98.139.244.58/31 permit +98.139.244.60/31 permit +98.139.244.62 permit +98.139.244.132 permit +98.139.244.141 permit +98.139.244.142/31 permit +98.139.244.144/31 permit +98.139.244.146 permit +98.139.244.152 permit +98.139.244.161 permit +98.139.244.162/31 permit +98.139.244.164/31 permit +98.139.244.166 permit +98.139.244.172 permit +98.139.244.181 permit +98.139.244.182/31 permit +98.139.244.184/31 permit +98.139.244.186 permit +98.139.244.192 permit +98.139.244.201 permit +98.139.244.202/31 permit +98.139.244.204/31 permit +98.139.244.206 permit +98.139.244.212 permit +98.139.244.221 permit +98.139.244.222/31 permit +98.139.244.224/31 permit +98.139.244.226 permit +98.139.244.232 permit +98.139.245.176/30 permit +98.139.245.180/31 permit +98.139.245.208/30 permit +98.139.245.212/31 permit +99.78.197.208/28 permit +99.83.190.102 permit +103.9.96.0/22 permit +103.28.42.0/24 permit +103.151.192.0/23 permit +103.168.172.128/27 permit +103.237.104.0/22 permit +104.43.243.237 permit +104.44.112.128/25 permit +104.47.0.0/17 permit +104.47.20.0/23 permit +104.47.75.0/24 permit +104.47.108.0/23 permit +104.130.96.0/28 permit +104.130.122.0/23 permit +106.10.144.64/27 permit +106.10.144.100/31 permit +106.10.144.103 permit +106.10.144.112/28 permit +106.10.144.128 permit +106.10.144.133 permit +106.10.144.138 permit +106.10.144.143 permit +106.10.144.148 permit +106.10.144.153 permit +106.10.144.158 permit +106.10.144.163 permit +106.10.144.168 permit +106.10.145.33 permit +106.10.145.34/31 permit +106.10.145.160/31 permit +106.10.145.162 permit +106.10.145.192/30 permit +106.10.145.196/31 permit +106.10.145.224/30 permit +106.10.145.228/31 permit +106.10.146.48/30 permit +106.10.146.52/31 permit +106.10.146.224/30 permit +106.10.146.228/31 permit +106.10.148.48/30 permit +106.10.148.52/31 permit +106.10.148.68/30 permit +106.10.148.80/28 permit +106.10.148.100/30 permit +106.10.148.124/31 permit +106.10.148.252/31 permit +106.10.148.254 permit +106.10.149.28/31 permit +106.10.149.30 permit +106.10.149.160/30 permit +106.10.149.164/31 permit +106.10.150.23 permit +106.10.150.24/30 permit +106.10.150.28/31 permit +106.10.150.32/30 permit +106.10.150.36/31 permit +106.10.150.52/30 permit +106.10.150.56/31 permit +106.10.150.72/30 permit +106.10.150.76/31 permit +106.10.150.92/30 permit +106.10.150.96/31 permit +106.10.150.112/30 permit +106.10.150.116/31 permit +106.10.150.132/30 permit +106.10.150.136/31 permit +106.10.150.172/30 permit +106.10.151.15 permit +106.10.151.16/31 permit +106.10.151.18 permit +106.10.151.19 permit +106.10.151.20 permit +106.10.151.21 permit +106.10.151.22/31 permit +106.10.151.24/31 permit +106.10.151.26/31 permit +106.10.151.28/30 permit +106.10.151.122/31 permit +106.10.151.138/31 permit +106.10.151.154/31 permit +106.10.151.170/31 permit +106.10.151.186/31 permit +106.10.151.202/31 permit +106.10.151.218/31 permit +106.10.151.234/31 permit +106.10.151.239 permit +106.10.151.250/31 permit +106.10.151.252/31 permit +106.10.151.254 permit +106.10.167.72 permit +106.10.167.128/27 permit +106.10.167.160/28 permit +106.10.167.176/31 permit +106.10.167.244/31 permit +106.10.167.246 permit +106.10.169.16/30 permit +106.10.169.20 permit +106.10.169.21 permit +106.10.169.208/31 permit +106.10.169.210/31 permit +106.10.169.233 permit +106.10.169.234/31 permit +106.10.169.236/31 permit +106.10.169.238 permit +106.10.169.253 permit +106.10.169.254 permit +106.10.174.118/31 permit +106.10.174.120/30 permit +106.10.174.154/31 permit +106.10.174.156/30 permit +106.10.176.32/29 permit +106.10.176.48 permit +106.10.176.112 permit +106.10.176.128/29 permit +106.10.176.136 permit +106.10.176.255 permit +106.10.177.0 permit +106.10.177.108/30 permit +106.10.177.128/30 permit +106.10.177.184/30 permit +106.10.177.188/31 permit +106.10.177.217 permit +106.10.177.218/31 permit +106.10.177.220/31 permit +106.10.177.222 permit +106.10.196.43 permit +106.10.196.44/30 permit +106.10.196.48 permit +106.10.240.0/24 permit +106.10.241.0/24 permit +106.10.242.0/24 permit +106.10.243.0/24 permit +106.10.244.0/24 permit +106.39.212.64/29 permit +106.50.16.0/28 permit +107.20.18.111 permit +107.20.210.250 permit +108.174.0.0/24 permit +108.174.0.215 permit +108.174.3.0/24 permit +108.174.3.215 permit +108.174.6.0/24 permit +108.174.6.215 permit +108.175.18.45 permit +108.175.30.45 permit +108.177.8.0/21 permit +108.177.96.0/19 permit +108.179.144.0/20 permit +109.237.142.0/24 permit +111.221.23.128/25 permit +111.221.26.0/27 permit +111.221.66.0/25 permit +111.221.69.128/25 permit +111.221.112.0/21 permit +112.19.199.64/29 permit +112.19.242.64/29 permit +116.214.12.47 permit +116.214.12.48/31 permit +116.214.12.56/31 permit +116.214.12.58 permit +116.214.12.73 permit +116.214.12.74/31 permit +116.214.12.76/31 permit +116.214.12.78 permit +116.214.12.93 permit +116.214.12.94/31 permit +116.214.12.96/31 permit +116.214.12.98 permit +117.120.16.0/21 permit +119.42.242.52/31 permit +119.42.242.156 permit +123.126.78.64/29 permit +124.108.96.24/31 permit +124.108.96.28/31 permit +124.108.96.70/31 permit +124.108.96.72/31 permit +128.17.0.0/20 permit +128.17.64.0/20 permit +128.17.128.0/20 permit +128.17.192.0/20 permit +128.127.70.0/26 permit +128.245.0.0/20 permit +128.245.64.0/20 permit +128.245.176.0/20 permit +128.245.240.0/24 permit +128.245.241.0/24 permit +128.245.242.0/24 permit +128.245.242.16 permit +128.245.242.17 permit +128.245.242.18 permit +128.245.243.0/24 permit +128.245.244.0/24 permit +128.245.245.0/24 permit +128.245.246.0/24 permit +128.245.247.0/24 permit +128.245.248.0/21 permit +129.41.77.70 permit +129.41.169.249 permit +129.80.5.164 permit +129.80.64.36 permit +129.80.67.121 permit +129.80.145.156 permit +129.145.74.12 permit +129.146.88.28 permit +129.146.147.105 permit +129.146.236.58 permit +129.151.67.221 permit +129.153.62.216 permit +129.153.104.71 permit +129.153.168.146 permit +129.153.190.200 permit +129.153.194.228 permit +129.154.255.129 permit +129.158.56.255 permit +129.159.22.159 permit +129.159.87.137 permit +129.213.195.191 permit +130.61.9.72 permit +130.162.39.83 permit +130.211.0.0/22 permit +130.248.172.0/24 permit +130.248.173.0/24 permit +131.253.30.0/24 permit +131.253.121.0/26 permit +132.145.13.209 permit +132.226.26.225 permit +132.226.49.32 permit +132.226.56.24 permit +134.170.27.8 permit +134.170.113.0/26 permit +134.170.141.64/26 permit +134.170.143.0/24 permit +134.170.174.0/24 permit +135.84.216.0/22 permit +136.147.128.0/20 permit +136.147.135.0/24 permit +136.147.176.0/20 permit +136.147.176.0/24 permit +136.147.182.0/24 permit +136.147.224.0/20 permit +136.179.50.206 permit +139.60.152.0/22 permit +139.138.35.44 permit +139.138.46.121 permit +139.138.46.176 permit +139.138.46.219 permit +139.138.57.55 permit +139.138.58.119 permit +139.180.17.0/24 permit +140.238.148.191 permit +141.148.159.229 permit +141.193.32.0/23 permit +141.193.184.32/27 permit +141.193.184.64/26 permit +141.193.184.128/25 permit +141.193.185.32/27 permit +141.193.185.64/26 permit +141.193.185.128/25 permit +143.47.120.152 permit +143.55.224.0/21 permit +143.55.232.0/22 permit +143.55.236.0/22 permit +143.244.80.0/20 permit +144.24.6.140 permit +144.34.8.247 permit +144.34.9.247 permit +144.34.32.247 permit +144.34.33.247 permit +144.178.36.0/24 permit +144.178.38.0/24 permit +145.253.228.160/29 permit +145.253.239.128/29 permit +146.20.14.104 permit +146.20.14.105 permit +146.20.14.106 permit +146.20.14.107 permit +146.20.112.0/26 permit +146.20.113.0/24 permit +146.20.191.0/24 permit +146.20.215.0/24 permit +146.20.215.182 permit +146.88.28.0/24 permit +147.154.32.0/25 permit +147.243.1.47 permit +147.243.1.48 permit +147.243.1.153 permit +147.243.128.24 permit +147.243.128.26 permit +148.105.0.0/16 permit +148.105.8.0/21 permit +149.72.0.0/16 permit +149.72.223.204 permit +149.72.248.236 permit +149.97.173.180 permit +150.230.98.160 permit +151.145.38.14 permit +152.67.105.195 permit +152.69.200.236 permit +152.70.155.126 permit +155.248.208.51 permit +155.248.220.138 permit +155.248.234.149 permit +155.248.237.141 permit +157.55.0.192/26 permit +157.55.1.128/26 permit +157.55.2.0/25 permit +157.55.9.128/25 permit +157.55.11.0/25 permit +157.55.49.0/25 permit +157.55.61.0/24 permit +157.55.157.128/25 permit +157.55.225.0/25 permit +157.56.24.0/25 permit +157.56.120.128/26 permit +157.56.232.0/21 permit +157.56.240.0/20 permit +157.56.248.0/21 permit +157.58.30.128/25 permit +157.58.196.96/29 permit +157.58.249.3 permit +157.151.208.65 permit +157.255.1.64/29 permit +158.101.211.207 permit +158.247.16.0/20 permit +159.92.154.0/24 permit +159.92.155.0/24 permit +159.92.157.0/24 permit +159.92.157.16 permit +159.92.157.17 permit +159.92.157.18 permit +159.92.158.0/24 permit +159.92.159.0/24 permit +159.92.160.0/24 permit +159.92.161.0/24 permit +159.92.162.0/24 permit +159.92.163.0/24 permit +159.92.164.0/22 permit +159.92.168.0/21 permit +159.112.240.0/20 permit +159.112.242.162 permit +159.135.132.128/25 permit +159.135.140.80/29 permit +159.135.224.0/20 permit +159.135.228.10 permit +159.183.0.0/16 permit +159.183.68.71 permit +159.183.79.38 permit +160.1.62.192 permit +161.38.192.0/20 permit +161.38.204.0/22 permit +161.71.32.0/19 permit +161.71.64.0/20 permit +162.88.4.0/23 permit +162.88.8.0/24 permit +162.88.24.0/24 permit +162.88.25.0/24 permit +162.88.36.0/24 permit +162.247.216.0/22 permit +163.47.180.0/22 permit +163.114.130.16 permit +163.114.132.120 permit +163.114.134.16 permit +163.114.135.16 permit +164.152.23.32 permit +164.177.132.168/30 permit +166.78.68.0/22 permit +166.78.68.221 permit +166.78.69.169 permit +166.78.69.170 permit +166.78.71.131 permit +167.89.0.0/17 permit +167.89.46.159 permit +167.89.54.103 permit +167.89.60.95 permit +167.89.64.9 permit +167.89.65.0 permit +167.89.65.53 permit +167.89.65.100 permit +167.89.74.233 permit +167.89.75.33 permit +167.89.75.126 permit +167.89.75.136 permit +167.89.75.164 permit +167.89.101.2 permit +167.89.101.192/28 permit +167.220.67.232/29 permit +168.138.5.36 permit +168.138.73.51 permit +168.138.77.31 permit +168.245.0.0/17 permit +168.245.12.252 permit +168.245.46.9 permit +168.245.127.231 permit +170.10.128.0/24 permit +170.10.129.0/24 permit +170.10.132.56/29 permit +170.10.132.64/29 permit +170.10.133.0/24 permit +172.217.0.0/19 permit +172.217.32.0/20 permit +172.217.128.0/19 permit +172.217.160.0/20 permit +172.217.192.0/19 permit +172.253.56.0/21 permit +172.253.112.0/20 permit +173.0.84.0/29 permit +173.0.84.224/27 permit +173.0.94.244/30 permit +173.194.0.0/16 permit +173.203.79.182 permit +173.203.81.39 permit +173.224.161.128/25 permit +173.224.165.0/26 permit +174.36.84.8/29 permit +174.36.84.16/29 permit +174.36.84.32/29 permit +174.36.84.144/29 permit +174.36.84.240/29 permit +174.36.85.248/30 permit +174.36.114.128/30 permit +174.36.114.140/30 permit +174.36.114.148/30 permit +174.36.114.152/29 permit +174.37.67.28/30 permit +175.41.215.51 permit +176.32.105.0/24 permit +176.32.127.0/24 permit +178.236.10.128/26 permit +182.50.76.0/22 permit +182.50.78.64/28 permit +183.240.219.64/29 permit +185.4.120.0/22 permit +185.12.80.0/22 permit +185.28.196.0/22 permit +185.58.84.93 permit +185.80.93.204 permit +185.80.93.227 permit +185.80.95.31 permit +185.90.20.0/22 permit +185.138.56.128/25 permit +185.189.236.0/22 permit +185.211.120.0/22 permit +185.250.236.0/22 permit +185.250.239.148 permit +185.250.239.168 permit +185.250.239.190 permit +188.125.68.132 permit +188.125.68.152/31 permit +188.125.68.156 permit +188.125.68.163 permit +188.125.68.172/31 permit +188.125.68.176 permit +188.125.68.179 permit +188.125.68.184 permit +188.125.68.186 permit +188.125.68.192 permit +188.125.69.105 permit +188.125.69.110 permit +188.125.69.112 permit +188.125.69.120 permit +188.125.69.126 permit +188.125.69.131 permit +188.125.69.132/31 permit +188.125.69.137 permit +188.125.69.139 permit +188.125.69.144/30 permit +188.125.69.148/31 permit +188.125.83.88/30 permit +188.125.83.92/31 permit +188.125.83.96/30 permit +188.125.83.100/31 permit +188.125.83.116/30 permit +188.125.83.120/31 permit +188.125.83.140/30 permit +188.125.83.144/31 permit +188.125.83.160/30 permit +188.125.83.164/31 permit +188.125.84.32/30 permit +188.125.84.36 permit +188.125.84.122/31 permit +188.125.84.124/30 permit +188.125.84.244/30 permit +188.125.84.248/31 permit +188.125.85.116/30 permit +188.125.85.120/31 permit +188.125.85.130/31 permit +188.125.85.132/30 permit +188.125.85.202/31 permit +188.125.85.204/31 permit +188.125.85.206 permit +188.125.85.233 permit +188.125.85.234/31 permit +188.125.85.236/31 permit +188.125.85.238 permit +188.172.128.0/20 permit +192.0.64.0/18 permit +192.18.139.154 permit +192.18.145.36 permit +192.18.152.58 permit +192.28.128.0/18 permit +192.29.103.128/25 permit +192.30.252.0/22 permit +192.161.144.0/20 permit +192.162.87.0/24 permit +192.237.158.0/23 permit +192.237.159.42 permit +192.237.159.43 permit +192.254.112.0/20 permit +192.254.112.60 permit +192.254.112.98/31 permit +192.254.113.10 permit +192.254.113.101 permit +192.254.114.176 permit +193.109.254.0/23 permit +193.122.128.100 permit +193.123.56.63 permit +194.19.134.0/25 permit +194.64.234.129 permit +194.106.220.0/23 permit +194.113.24.0/22 permit +194.154.193.192/27 permit +195.4.92.0/23 permit +195.54.172.0/23 permit +195.234.109.226 permit +195.245.230.0/23 permit +198.2.128.0/18 permit +198.21.0.0/21 permit +198.37.144.0/20 permit +198.37.152.186 permit +198.61.254.0/23 permit +198.61.254.21 permit +198.61.254.231 permit +198.178.234.57 permit +198.244.48.0/20 permit +198.244.59.30 permit +198.244.59.33 permit +198.244.59.35 permit +198.244.60.0/22 permit +198.245.80.0/20 permit +198.245.81.0/24 permit +199.15.212.0/22 permit +199.15.213.187 permit +199.15.226.37 permit +199.16.156.0/22 permit +199.33.145.1 permit +199.33.145.32 permit +199.59.148.0/22 permit +199.101.161.130 permit +199.101.162.0/25 permit +199.122.120.0/21 permit +199.122.123.0/24 permit +199.127.232.0/22 permit +199.255.192.0/22 permit +202.12.124.128/27 permit +202.129.242.0/23 permit +202.165.102.47 permit +202.177.148.100 permit +202.177.148.110 permit +203.32.4.25 permit +203.55.21.0/24 permit +203.81.17.0/24 permit +203.122.32.250 permit +203.145.57.160/27 permit +203.188.194.32 permit +203.188.194.151 permit +203.188.194.203 permit +203.188.194.204 permit +203.188.194.213 permit +203.188.194.251 permit +203.188.195.240/30 permit +203.188.195.244/31 permit +203.188.197.193 permit +203.188.197.194/31 permit +203.188.197.196/30 permit +203.188.197.209 permit +203.188.197.210/31 permit +203.188.197.212/30 permit +203.188.197.216/29 permit +203.188.197.232/29 permit +203.188.197.240/29 permit +203.188.200.56/31 permit +203.188.200.58 permit +203.188.200.60/30 permit +203.188.200.83 permit +203.188.200.100/31 permit +203.188.200.102 permit +203.188.200.108/31 permit +203.188.200.160/31 permit +203.188.200.208/30 permit +203.188.200.212/31 permit +203.188.201.12/30 permit +203.209.230.75 permit +203.209.230.76/31 permit +204.11.168.0/21 permit +204.13.11.48/29 permit +204.14.232.0/21 permit +204.14.232.64/28 permit +204.14.234.64/28 permit +204.75.142.0/24 permit +204.79.197.212 permit +204.92.114.187 permit +204.92.114.203 permit +204.92.114.204/31 permit +204.220.160.0/21 permit +204.220.168.0/21 permit +204.220.176.0/20 permit +204.232.168.0/24 permit +205.139.110.0/24 permit +205.201.128.0/20 permit +205.201.137.229 permit +205.207.104.0/22 permit +205.220.167.17 permit +205.220.167.98 permit +205.220.179.17 permit +205.220.179.98 permit +205.251.233.32 permit +205.251.233.36 permit +206.25.247.143 permit +206.25.247.155 permit +206.55.144.0/20 permit +206.165.246.80/29 permit +206.191.224.0/19 permit +206.246.157.1 permit +207.46.4.128/25 permit +207.46.22.35 permit +207.46.50.72 permit +207.46.50.82 permit +207.46.50.192/26 permit +207.46.50.224 permit +207.46.52.71 permit +207.46.52.79 permit +207.46.58.128/25 permit +207.46.116.128/29 permit +207.46.117.0/24 permit +207.46.132.128/27 permit +207.46.198.0/25 permit +207.46.200.0/27 permit +207.67.38.0/24 permit +207.67.98.192/27 permit +207.68.176.0/26 permit +207.68.176.96/27 permit +207.97.204.96/29 permit +207.126.144.0/20 permit +207.171.160.0/19 permit +207.211.30.64/26 permit +207.211.30.128/25 permit +207.211.31.0/25 permit +207.211.41.113 permit +207.218.90.0/24 permit +207.218.90.122 permit +207.250.68.0/24 permit +208.40.232.70 permit +208.43.21.28/30 permit +208.43.21.64/29 permit +208.43.21.72/30 permit +208.64.132.0/22 permit +208.71.40.63 permit +208.71.40.64/31 permit +208.71.40.174/31 permit +208.71.40.185 permit +208.71.40.186/31 permit +208.71.40.202/31 permit +208.71.40.204/31 permit +208.71.40.219 permit +208.71.41.21 permit +208.71.41.22/31 permit +208.71.41.24/31 permit +208.71.41.42/31 permit +208.71.41.64/30 permit +208.71.41.68/31 permit +208.71.41.171 permit +208.71.41.172/31 permit +208.71.41.188/30 permit +208.71.41.192/31 permit +208.71.42.190/31 permit +208.71.42.192/28 permit +208.71.42.208/30 permit +208.71.42.212/31 permit +208.71.42.214 permit +208.72.249.240/29 permit +208.74.204.5 permit +208.74.204.9 permit +208.75.120.0/22 permit +208.76.62.0/24 permit +208.76.63.0/24 permit +208.82.237.96/29 permit +208.82.237.104/31 permit +208.82.238.96/29 permit +208.82.238.104/31 permit +208.85.50.137 permit +208.117.48.0/20 permit +208.185.229.45 permit +208.201.241.163 permit +209.43.22.0/28 permit +209.46.117.168 permit +209.46.117.179 permit +209.61.151.0/24 permit +209.61.151.236 permit +209.61.151.249 permit +209.61.151.251 permit +209.67.98.46 permit +209.67.98.59 permit +209.85.128.0/17 permit +212.82.96.32/27 permit +212.82.96.64/29 permit +212.82.98.32/29 permit +212.82.98.64/27 permit +212.82.98.96/30 permit +212.82.98.100/30 permit +212.82.98.104/29 permit +212.82.98.112/29 permit +212.82.98.120/30 permit +212.82.98.144/28 permit +212.82.105.240/30 permit +212.82.108.112/29 permit +212.82.108.120/30 permit +212.82.108.124/31 permit +212.82.108.126 permit +212.82.108.132/30 permit +212.82.108.136/30 permit +212.82.108.208/30 permit +212.82.108.212/31 permit +212.82.108.224/30 permit +212.82.108.232/29 permit +212.82.108.240/29 permit +212.82.108.248/30 permit +212.82.108.252/31 permit +212.82.108.254 permit +212.82.109.128/31 permit +212.82.109.132 permit +212.82.111.131 permit +212.82.111.133 permit +212.82.111.135 permit +212.82.111.137 permit +212.82.111.139 permit +212.82.111.141 permit +212.82.111.225 permit +212.82.111.226/31 permit +212.82.111.228/31 permit +212.82.111.230 permit +212.123.28.40 permit +212.227.15.3 permit +212.227.15.4 permit +212.227.15.5 permit +212.227.15.6 permit +212.227.15.14 permit +212.227.15.15 permit +212.227.15.18 permit +212.227.15.19 permit +212.227.15.25 permit +212.227.15.26 permit +212.227.15.29 permit +212.227.15.44 permit +212.227.15.45 permit +212.227.15.46 permit +212.227.15.47 permit +212.227.15.50 permit +212.227.15.52 permit +212.227.15.53 permit +212.227.15.54 permit +212.227.15.55 permit +212.227.17.11 permit +212.227.17.12 permit +212.227.17.18 permit +212.227.17.19 permit +212.227.17.20 permit +212.227.17.21 permit +212.227.17.22 permit +212.227.17.26 permit +212.227.17.28 permit +212.227.17.29 permit +212.227.126.224 permit +212.227.126.225 permit +212.227.126.226 permit +212.227.126.227 permit +213.46.255.0/24 permit +213.199.128.139 permit +213.199.128.145 permit +213.199.138.181 permit +213.199.138.191 permit +213.199.161.128/27 permit +213.199.177.0/26 permit +216.17.150.242 permit +216.17.150.251 permit +216.24.224.0/20 permit +216.39.60.154/31 permit +216.39.60.156/30 permit +216.39.60.160/30 permit +216.39.60.164 permit +216.39.60.192/28 permit +216.39.60.208/29 permit +216.39.60.216 permit +216.39.60.218 permit +216.39.60.230/31 permit +216.39.60.232/29 permit +216.39.60.240/29 permit +216.39.60.248/30 permit +216.39.60.252/31 permit +216.39.60.254 permit +216.39.61.170 permit +216.39.61.175 permit +216.39.61.238/31 permit +216.39.62.32/28 permit +216.39.62.48/29 permit +216.39.62.56/30 permit +216.39.62.60/31 permit +216.39.62.136/29 permit +216.39.62.144/31 permit +216.58.192.0/19 permit +216.66.217.240/29 permit +216.71.138.33 permit +216.71.152.207 permit +216.71.154.29 permit +216.71.155.89 permit +216.74.162.13 permit +216.74.162.14 permit +216.82.240.0/20 permit +216.98.158.0/24 permit +216.99.5.67 permit +216.99.5.68 permit +216.109.114.32/27 permit +216.109.114.64/29 permit +216.128.126.97 permit +216.136.162.65 permit +216.136.162.120/29 permit +216.136.168.80/28 permit +216.139.64.0/19 permit +216.145.221.0/24 permit +216.146.32.0/24 permit +216.146.33.0/24 permit +216.198.0.0/18 permit +216.203.30.55 permit +216.203.33.178/31 permit +216.205.24.0/24 permit +216.221.160.0/19 permit +216.239.32.0/19 permit +217.72.192.77 permit +217.72.192.78 permit +217.77.141.52 permit +217.77.141.59 permit +217.175.194.0/24 permit +222.73.195.64/29 permit +223.165.113.0/24 permit +223.165.115.0/24 permit +223.165.118.0/23 permit +223.165.120.0/23 permit +2001:0868:0100:0600::/64 permit +2001:4860:4000::/36 permit +2001:748:100:40::2:0/112 permit +2404:6800:4000::/36 permit +2603:1010:3:3::5b permit +2603:1020:201:10::10f permit +2603:1030:20e:3::23c permit +2603:1030:b:3::152 permit +2603:1030:c02:8::14 permit +2607:f8b0:4000::/36 permit +2620:109:c003:104::/64 permit +2620:109:c003:104::215 permit +2620:109:c006:104::/64 permit +2620:109:c006:104::215 permit +2620:109:c00d:104::/64 permit +2620:10d:c090:400::8:1 permit +2620:10d:c091:400::8:1 permit +2620:10d:c09b:400::8:1 permit +2620:10d:c09c:400::8:1 permit +2620:119:50c0:207::/64 permit +2620:119:50c0:207::215 permit +2800:3f0:4000::/36 permit +194.25.134.0/24 permit # t-online.de diff --git a/mailcow/data/conf/postfix/smtp_dsn_filter b/mailcow/data/conf/postfix/smtp_dsn_filter new file mode 100644 index 0000000..2fb5101 --- /dev/null +++ b/mailcow/data/conf/postfix/smtp_dsn_filter @@ -0,0 +1,6 @@ +/^4(\.\d+\.\d+ TLS is required, but host \S+ refused to start TLS: .+)/ + 5$1 +/^4(\.\d+\.\d+ TLS is required, but was not offered by host .+)/ + 5$1 +/^4.7.5(.*)/ + 5.7.5$1 diff --git a/mailcow/data/conf/redis/redis-conf.sh b/mailcow/data/conf/redis/redis-conf.sh new file mode 100755 index 0000000..7e2672a --- /dev/null +++ b/mailcow/data/conf/redis/redis-conf.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +cat < /redis.conf +requirepass $REDISPASS +user quota_notify on nopass ~QW_* -@all +get +hget +ping +EOF + +if [ -n "$REDISMASTERPASS" ]; then + echo "masterauth $REDISMASTERPASS" >> /redis.conf +fi + +exec redis-server /redis.conf diff --git a/mailcow/data/conf/rspamd/dynmaps/aliasexp.php b/mailcow/data/conf/rspamd/dynmaps/aliasexp.php new file mode 100644 index 0000000..824037c --- /dev/null +++ b/mailcow/data/conf/rspamd/dynmaps/aliasexp.php @@ -0,0 +1,175 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, +]; +try { + $pdo = new PDO($dsn, $database_user, $database_pass, $opt); +} +catch (PDOException $e) { + error_log("ALIASEXP: " . $e . PHP_EOL); + http_response_code(501); + exit; +} + +// Init Redis +$redis = new Redis(); +$redis->connect('redis-mailcow', 6379); +$redis->auth(getenv("REDISPASS")); + +function parse_email($email) { + if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false; + $a = strrpos($email, '@'); + return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1)); +} +if (!function_exists('getallheaders')) { + function getallheaders() { + if (!is_array($_SERVER)) { + return array(); + } + $headers = array(); + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) == 'HTTP_') { + $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + } + } + return $headers; + } +} + +// Read headers +$headers = getallheaders(); +// Get rcpt +$rcpt = $headers['Rcpt']; +// Remove tag +$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt); +// Parse email address +$parsed_rcpt = parse_email($rcpt); +// Create array of final mailboxes +$rcpt_final_mailboxes = array(); + +// Skip if not a mailcow handled domain +try { + if (!$redis->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) { + exit; + } +} +catch (RedisException $e) { + error_log("ALIASEXP: " . $e . PHP_EOL); + http_response_code(504); + exit; +} + +// Always assume rcpt is not a final mailbox but an alias for a mailbox or further aliases +// +// rcpt +// | +// mailbox <-- goto ---> alias1, alias2, mailbox2 +// | | +// mailbox3 | +// | +// alias3 ---> mailbox4 +// +try { + $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'"); + $stmt->execute(array( + ':rcpt' => $rcpt + )); + $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; + if (empty($gotos)) { + $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'"); + $stmt->execute(array( + ':rcpt' => '@' . $parsed_rcpt['domain'] + )); + $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; + } + if (empty($gotos)) { + $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :rcpt AND `active` = '1'"); + $stmt->execute(array(':rcpt' => $parsed_rcpt['domain'])); + $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain']; + if ($goto_branch) { + $gotos = $parsed_rcpt['local'] . '@' . $goto_branch; + } + } + $gotos_array = explode(',', $gotos); + + $loop_c = 0; + + while (count($gotos_array) != 0 && $loop_c <= 20) { + + // Loop through all found gotos + foreach ($gotos_array as $index => &$goto) { + error_log("ALIAS EXPANDER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL); + $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND (`active`= '1' OR `active`= '2');"); + $stmt->execute(array(':goto' => $goto)); + $username = $stmt->fetch(PDO::FETCH_ASSOC)['username']; + if (!empty($username)) { + error_log("ALIAS EXPANDER: http pipe: mailbox found: " . $username . PHP_EOL); + // Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate + if (!in_array($username, $rcpt_final_mailboxes)) { + $rcpt_final_mailboxes[] = $username; + } + } + else { + $parsed_goto = parse_email($goto); + if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) { + error_log("ALIAS EXPANDER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL); + } + else { + $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'"); + $stmt->execute(array(':goto' => $goto)); + $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; + if ($goto_branch) { + error_log("ALIAS EXPANDER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL); + $goto_branch_array = explode(',', $goto_branch); + } else { + $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'"); + $stmt->execute(array(':domain' => $parsed_goto['domain'])); + $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain']; + if ($goto_branch) { + error_log("ALIAS EXPANDER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL); + $goto_branch_array = array($parsed_goto['local'] . '@' . $goto_branch); + } + } + } + } + // goto item was processed, unset + unset($gotos_array[$index]); + } + + // Merge goto branch array derived from previous loop (if any), filter duplicates and unset goto branch array + if (!empty($goto_branch_array)) { + $gotos_array = array_unique(array_merge($gotos_array, $goto_branch_array)); + unset($goto_branch_array); + } + + // Reindex array + $gotos_array = array_values($gotos_array); + + // Force exit if loop cannot be solved + // Postfix does not allow for alias loops, so this should never happen. + $loop_c++; + error_log("ALIAS EXPANDER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL); + } +} +catch (PDOException $e) { + error_log("ALIAS EXPANDER: " . $e->getMessage() . PHP_EOL); + http_response_code(502); + exit; +} + +// Does also return the mailbox name if question == answer (query == mailbox) +if (count($rcpt_final_mailboxes) == 1) { + error_log("ALIASEXP: direct alias " . $rcpt . " expanded to " . $rcpt_final_mailboxes[0] . PHP_EOL); + echo trim($rcpt_final_mailboxes[0]); +} diff --git a/mailcow/data/conf/rspamd/dynmaps/bcc.php b/mailcow/data/conf/rspamd/dynmaps/bcc.php new file mode 100644 index 0000000..3145fee --- /dev/null +++ b/mailcow/data/conf/rspamd/dynmaps/bcc.php @@ -0,0 +1,88 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, +]; +try { + $pdo = new PDO($dsn, $database_user, $database_pass, $opt); +} +catch (PDOException $e) { + error_log("BCC MAP SQL ERROR: " . $e . PHP_EOL); + http_response_code(501); + exit; +} + +function parse_email($email) { + if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false; + $a = strrpos($email, '@'); + return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1)); +} +if (!function_exists('getallheaders')) { + function getallheaders() { + if (!is_array($_SERVER)) { + return array(); + } + $headers = array(); + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) == 'HTTP_') { + $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + } + } + return $headers; + } +} + +// Read headers +$headers = getallheaders(); +// Get rcpt +$rcpt = $headers['Rcpt']; +// Get from +$from = $headers['From']; +// Remove tags +$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt); +$from = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $from); + +try { + if (!empty($rcpt)) { + $stmt = $pdo->prepare("SELECT `bcc_dest` FROM `bcc_maps` WHERE `type` = 'rcpt' AND `local_dest` = :local_dest AND `active` = '1'"); + $stmt->execute(array( + ':local_dest' => $rcpt + )); + $bcc_dest = $stmt->fetch(PDO::FETCH_ASSOC)['bcc_dest']; + if (!empty($bcc_dest) && filter_var($bcc_dest, FILTER_VALIDATE_EMAIL)) { + error_log("BCC MAP: returning ". $bcc_dest . " for " . $rcpt . PHP_EOL); + http_response_code(201); + echo trim($bcc_dest); + exit; + } + } + if (!empty($from)) { + $stmt = $pdo->prepare("SELECT `bcc_dest` FROM `bcc_maps` WHERE `type` = 'sender' AND `local_dest` = :local_dest AND `active` = '1'"); + $stmt->execute(array( + ':local_dest' => $from + )); + $bcc_dest = $stmt->fetch(PDO::FETCH_ASSOC)['bcc_dest']; + if (!empty($bcc_dest) && filter_var($bcc_dest, FILTER_VALIDATE_EMAIL)) { + error_log("BCC MAP: returning ". $bcc_dest . " for " . $from . PHP_EOL); + http_response_code(201); + echo trim($bcc_dest); + exit; + } + } +} +catch (PDOException $e) { + error_log("BCC MAP SQL ERROR: " . $e->getMessage() . PHP_EOL); + http_response_code(502); + exit; +} + diff --git a/mailcow/data/conf/rspamd/dynmaps/footer.php b/mailcow/data/conf/rspamd/dynmaps/footer.php new file mode 100644 index 0000000..545c45e --- /dev/null +++ b/mailcow/data/conf/rspamd/dynmaps/footer.php @@ -0,0 +1,113 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, +]; +try { + $pdo = new PDO($dsn, $database_user, $database_pass, $opt); +} +catch (PDOException $e) { + error_log("FOOTER: " . $e . PHP_EOL); + http_response_code(501); + exit; +} + +if (!function_exists('getallheaders')) { + function getallheaders() { + if (!is_array($_SERVER)) { + return array(); + } + $headers = array(); + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) == 'HTTP_') { + $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + } + } + return $headers; + } +} + +// Read headers +$headers = getallheaders(); +// Get Domain +$domain = $headers['Domain']; +// Get Username +$username = $headers['Username']; +// Get From +$from = $headers['From']; +// define empty footer +$empty_footer = json_encode(array( + 'html' => '', + 'plain' => '', + 'skip_replies' => 0, + 'vars' => array() +)); + +error_log("FOOTER: checking for domain " . $domain . ", user " . $username . " and address " . $from . PHP_EOL); + +try { + // try get $target_domain if $domain is an alias_domain + $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` + WHERE `alias_domain` = :alias_domain"); + $stmt->execute(array( + ':alias_domain' => $domain + )); + $alias_domain = $stmt->fetch(PDO::FETCH_ASSOC); + if (!$alias_domain) { + $target_domain = $domain; + } else { + $target_domain = $alias_domain['target_domain']; + } + + // get footer associated with the domain + $stmt = $pdo->prepare("SELECT `plain`, `html`, `mbox_exclude`, `alias_domain_exclude`, `skip_replies` FROM `domain_wide_footer` + WHERE `domain` = :domain"); + $stmt->execute(array( + ':domain' => $target_domain + )); + $footer = $stmt->fetch(PDO::FETCH_ASSOC); + + // check if the sender is excluded + if (in_array($from, json_decode($footer['mbox_exclude']))){ + $footer = false; + } + if (in_array($domain, json_decode($footer['alias_domain_exclude']))){ + $footer = false; + } + if (empty($footer)){ + echo $empty_footer; + exit; + } + error_log("FOOTER: " . json_encode($footer) . PHP_EOL); + + // footer will be applied + // get custom mailbox attributes to insert into the footer + $stmt = $pdo->prepare("SELECT `custom_attributes` FROM `mailbox` WHERE `username` = :username"); + $stmt->execute(array( + ':username' => $username + )); + $custom_attributes = $stmt->fetch(PDO::FETCH_ASSOC)['custom_attributes']; + if (empty($custom_attributes)){ + $custom_attributes = (object)array(); + } +} +catch (Exception $e) { + error_log("FOOTER: " . $e->getMessage() . PHP_EOL); + http_response_code(502); + exit; +} + + +// return footer +$footer["vars"] = $custom_attributes; +echo json_encode($footer); diff --git a/mailcow/data/conf/rspamd/dynmaps/forwardinghosts.php b/mailcow/data/conf/rspamd/dynmaps/forwardinghosts.php new file mode 100644 index 0000000..2186d7f --- /dev/null +++ b/mailcow/data/conf/rspamd/dynmaps/forwardinghosts.php @@ -0,0 +1,58 @@ +connect('redis-mailcow', 6379); +$redis->auth(getenv("REDISPASS")); + +function in_net($addr, $net) { + $net = explode('/', $net); + if (count($net) > 1) { + $mask = $net[1]; + } + $net = inet_pton($net[0]); + $addr = inet_pton($addr); + $length = strlen($net); // 4 for IPv4, 16 for IPv6 + if (strlen($net) != strlen($addr)) { + return false; + } + if (!isset($mask)) { + $mask = $length * 8; + } + $addr_bin = ''; + $net_bin = ''; + for ($i = 0; $i < $length; ++$i) { + $addr_bin .= str_pad(decbin(ord(substr($addr, $i, $i+1))), 8, '0', STR_PAD_LEFT); + $net_bin .= str_pad(decbin(ord(substr($net, $i, $i+1))), 8, '0', STR_PAD_LEFT); + } + return substr($addr_bin, 0, $mask) == substr($net_bin, 0, $mask); +} + +if (isset($_GET['host'])) { + try { + foreach ($redis->hGetAll('WHITELISTED_FWD_HOST') as $host => $source) { + if (in_net($_GET['host'], $host)) { + echo '200 PERMIT'; + exit; + } + } + echo '200 DUNNO'; + } + catch (RedisException $e) { + echo '200 DUNNO'; + exit; + } +} else { + try { + echo '240.240.240.240' . PHP_EOL; + foreach ($redis->hGetAll('WHITELISTED_FWD_HOST') as $host => $source) { + echo $host . PHP_EOL; + } + } + catch (RedisException $e) { + echo '240.240.240.240' . PHP_EOL; + exit; + } +} +?> diff --git a/mailcow/data/conf/rspamd/dynmaps/index.html b/mailcow/data/conf/rspamd/dynmaps/index.html new file mode 100644 index 0000000..90531a4 --- /dev/null +++ b/mailcow/data/conf/rspamd/dynmaps/index.html @@ -0,0 +1,2 @@ + + diff --git a/mailcow/data/conf/rspamd/dynmaps/sasl_logs.php b/mailcow/data/conf/rspamd/dynmaps/sasl_logs.php new file mode 100644 index 0000000..2d4cbe6 --- /dev/null +++ b/mailcow/data/conf/rspamd/dynmaps/sasl_logs.php @@ -0,0 +1,2 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, +]; +try { + $pdo = new PDO($dsn, $database_user, $database_pass, $opt); + $stmt = $pdo->query("SELECT '1' FROM `filterconf`"); +} +catch (PDOException $e) { + echo 'settings { }'; + exit; +} + +// Check if db changed and return header +$stmt = $pdo->prepare("SELECT GREATEST(COALESCE(MAX(UNIX_TIMESTAMP(UPDATE_TIME)), 1), COALESCE(MAX(UNIX_TIMESTAMP(CREATE_TIME)), 1)) AS `db_update_time` FROM `information_schema`.`tables` + WHERE (`TABLE_NAME` = 'filterconf' OR `TABLE_NAME` = 'settingsmap' OR `TABLE_NAME` = 'sogo_quick_contact' OR `TABLE_NAME` = 'alias') + AND TABLE_SCHEMA = :dbname;"); +$stmt->execute(array( + ':dbname' => $database_name +)); +$db_update_time = $stmt->fetch(PDO::FETCH_ASSOC)['db_update_time']; +if (empty($db_update_time)) { + $db_update_time = 1572048000; +} +if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && (strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $db_update_time)) { + header('Last-Modified: '.gmdate('D, d M Y H:i:s', $db_update_time).' GMT', true, 304); + exit; +} else { + header('Last-Modified: '.gmdate('D, d M Y H:i:s', $db_update_time).' GMT', true, 200); +} + +function parse_email($email) { + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) return false; + $a = strrpos($email, '@'); + return array('local' => substr($email, 0, $a), 'domain' => substr($email, $a)); +} + +function normalize_email($email) { + $email = strtolower(str_replace('/', '\/', $email)); + $gm = "@gmail.com"; + if (substr_compare($email, $gm, -strlen($gm)) == 0) { + $email = explode('@', $email); + $email[0] = str_replace('.', '', $email[0]); + $email = implode('@', $email); + } + $gm_alt = "@googlemail.com"; + if (substr_compare($email, $gm_alt, -strlen($gm_alt)) == 0) { + $email = explode('@', $email); + $email[0] = str_replace('.', '', $email[0]); + $email[1] = str_replace('@', '', $gm); + $email = implode('@', $email); + } + if (str_contains($email, "+")) { + $email = explode('@', $email); + $user = explode('+', $email[0]); + $email[0] = $user[0]; + $email = implode('@', $email); + } + return $email; +} + +function wl_by_sogo() { + global $pdo; + $rcpt = array(); + $stmt = $pdo->query("SELECT DISTINCT(`sogo_folder_info`.`c_path2`) AS `user`, GROUP_CONCAT(`sogo_quick_contact`.`c_mail`) AS `contacts` FROM `sogo_folder_info` + INNER JOIN `sogo_quick_contact` ON `sogo_quick_contact`.`c_folder_id` = `sogo_folder_info`.`c_folder_id` + GROUP BY `c_path2`"); + $sogo_contacts = $stmt->fetchAll(PDO::FETCH_ASSOC); + while ($row = array_shift($sogo_contacts)) { + foreach (explode(',', $row['contacts']) as $contact) { + if (!filter_var($contact, FILTER_VALIDATE_EMAIL)) { + continue; + } + // Explicit from, no mime_from, no regex - envelope must match + // mailcow white and blacklists also cover mime_from + $rcpt[$row['user']][] = normalize_email($contact); + } + } + return $rcpt; +} + +function ucl_rcpts($object, $type) { + global $pdo; + $rcpt = array(); + if ($type == 'mailbox') { + // Standard aliases + $stmt = $pdo->prepare("SELECT `address` FROM `alias` + WHERE `goto` = :object_goto + AND `address` NOT LIKE '@%'"); + $stmt->execute(array( + ':object_goto' => $object + )); + $standard_aliases = $stmt->fetchAll(PDO::FETCH_ASSOC); + while ($row = array_shift($standard_aliases)) { + $local = parse_email($row['address'])['local']; + $domain = parse_email($row['address'])['domain']; + if (!empty($local) && !empty($domain)) { + $rcpt[] = '/^' . str_replace('/', '\/', $local) . '[+].*' . str_replace('/', '\/', $domain) . '$/i'; + } + $rcpt[] = str_replace('/', '\/', $row['address']); + } + // Aliases by alias domains + $stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias` FROM `mailbox` + LEFT OUTER JOIN `alias_domain` ON `mailbox`.`domain` = `alias_domain`.`target_domain` + WHERE `mailbox`.`username` = :object"); + $stmt->execute(array( + ':object' => $object + )); + $by_domain_aliases = $stmt->fetchAll(PDO::FETCH_ASSOC); + array_filter($by_domain_aliases); + while ($row = array_shift($by_domain_aliases)) { + if (!empty($row['alias'])) { + $local = parse_email($row['alias'])['local']; + $domain = parse_email($row['alias'])['domain']; + if (!empty($local) && !empty($domain)) { + $rcpt[] = '/^' . str_replace('/', '\/', $local) . '[+].*' . str_replace('/', '\/', $domain) . '$/i'; + } + $rcpt[] = str_replace('/', '\/', $row['alias']); + } + } + } + elseif ($type == 'domain') { + // Domain self + $rcpt[] = '/.*@' . $object . '/i'; + $stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` + WHERE `target_domain` = :object"); + $stmt->execute(array(':object' => $object)); + $alias_domains = $stmt->fetchAll(PDO::FETCH_ASSOC); + array_filter($alias_domains); + while ($row = array_shift($alias_domains)) { + $rcpt[] = '/.*@' . $row['alias_domain'] . '/i'; + } + } + return $rcpt; +} +?> +settings { + watchdog { + priority = 10; + rcpt_mime = "/null@localhost/i"; + from_mime = "/watchdog@localhost/i"; + apply "default" { + symbols_disabled = ["HISTORY_SAVE", "ARC", "ARC_SIGNED", "DKIM", "DKIM_SIGNED", "CLAM_VIRUS"]; + want_spam = yes; + actions { + reject = 9999.0; + greylist = 9998.0; + "add header" = 9997.0; + } + + } + } +query("SELECT DISTINCT `object` FROM `filterconf` WHERE `option` = 'highspamlevel' OR `option` = 'lowspamlevel'"); +$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + +while ($row = array_shift($rows)) { + $username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['object']); +?> + score_ { + priority = 4; + + rcpt = ; +prepare("SELECT `option`, `value` FROM `filterconf` + WHERE (`option` = 'highspamlevel' OR `option` = 'lowspamlevel') + AND `object`= :object"); + $stmt->execute(array(':object' => $row['object'])); + $spamscore = $stmt->fetchAll(PDO::FETCH_COLUMN|PDO::FETCH_GROUP); +?> + apply "default" { + actions { + reject = ; + greylist = ; + "add header" = ; + } + } + } + $contacts) { + $username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $user); +?> + whitelist_sogo_ { + + from = ; + + priority = 4; + + rcpt = ; + + apply "default" { + SOGO_CONTACT = -99.0; + } + symbols [ + "SOGO_CONTACT" + ] + } +query("SELECT DISTINCT `object` FROM `filterconf` WHERE `option` = 'whitelist_from'"); +$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); +while ($row = array_shift($rows)) { + $username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['object']); +?> + whitelist_ { +prepare("SELECT `value` FROM `filterconf` + WHERE `object`= :object + AND `option` = 'whitelist_from'"); + $stmt->execute(array(':object' => $row['object'])); + $list_items = $stmt->fetchAll(PDO::FETCH_ASSOC); + foreach ($list_items as $item) { +?> + from = "//i"; + + priority = 5; + + rcpt = ; + + priority = 6; + + rcpt = ; + + apply "default" { + MAILCOW_WHITE = -999.0; + } + symbols [ + "MAILCOW_WHITE" + ] + } + whitelist_mime_ { + + from_mime = "//i"; + + priority = 5; + + rcpt = ; + + priority = 6; + + rcpt = ; + + apply "default" { + MAILCOW_WHITE = -999.0; + } + symbols [ + "MAILCOW_WHITE" + ] + } +query("SELECT DISTINCT `object` FROM `filterconf` WHERE `option` = 'blacklist_from'"); +$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); +while ($row = array_shift($rows)) { + $username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['object']); +?> + blacklist_ { +prepare("SELECT `value` FROM `filterconf` + WHERE `object`= :object + AND `option` = 'blacklist_from'"); + $stmt->execute(array(':object' => $row['object'])); + $list_items = $stmt->fetchAll(PDO::FETCH_ASSOC); + foreach ($list_items as $item) { +?> + from = "//i"; + + priority = 5; + + rcpt = ; + + priority = 6; + + rcpt = ; + + apply "default" { + MAILCOW_BLACK = 999.0; + } + symbols [ + "MAILCOW_BLACK" + ] + } + blacklist_header_ { + + from_mime = "//i"; + + priority = 5; + + rcpt = ; + + priority = 6; + + rcpt = ; + + apply "default" { + MAILCOW_BLACK = 999.0; + } + symbols [ + "MAILCOW_BLACK" + ] + } + + ham_trap { + + rcpt = ; + + priority = 9; + apply "default" { + symbols_enabled = ["HISTORY_SAVE"]; + } + symbols [ + "HAM_TRAP" + ] + } + + spam_trap { + + rcpt = ; + + priority = 9; + apply "default" { + symbols_enabled = ["HISTORY_SAVE"]; + } + symbols [ + "SPAM_TRAP" + ] + } +query("SELECT `id`, `content` FROM `settingsmap` WHERE `active` = '1'"); +$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); +while ($row = array_shift($rows)) { + $username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['id']); +?> + additional_settings_ { + + } + +} diff --git a/mailcow/data/conf/rspamd/dynmaps/vars.inc.php b/mailcow/data/conf/rspamd/dynmaps/vars.inc.php new file mode 100644 index 0000000..79566b0 --- /dev/null +++ b/mailcow/data/conf/rspamd/dynmaps/vars.inc.php @@ -0,0 +1,6 @@ + diff --git a/mailcow/data/conf/rspamd/lua/ratelimit.lua b/mailcow/data/conf/rspamd/lua/ratelimit.lua new file mode 100644 index 0000000..635fe3e --- /dev/null +++ b/mailcow/data/conf/rspamd/lua/ratelimit.lua @@ -0,0 +1,16 @@ +local custom_keywords = {} + +custom_keywords.mailcow = function(task) + local rspamd_logger = require "rspamd_logger" + local dyn_rl_symbol = task:get_symbol("DYN_RL") + if dyn_rl_symbol then + local rl_value = dyn_rl_symbol[1].options[1] + local rl_object = dyn_rl_symbol[1].options[2] + if rl_value and rl_object then + rspamd_logger.infox(rspamd_config, "DYN_RL symbol has value %s for object %s, returning %s...", rl_value, rl_object, "rs_dynrl_" .. rl_object) + return "rs_dynrl_" .. rl_object, rl_value + end + end +end + +return custom_keywords diff --git a/mailcow/data/conf/rspamd/lua/rspamd.local.lua b/mailcow/data/conf/rspamd/lua/rspamd.local.lua new file mode 100644 index 0000000..5f23ef6 --- /dev/null +++ b/mailcow/data/conf/rspamd/lua/rspamd.local.lua @@ -0,0 +1,701 @@ +rspamd_config.MAILCOW_AUTH = { + callback = function(task) + local uname = task:get_user() + if uname then + return 1 + end + end +} + +local monitoring_hosts = rspamd_config:add_map{ + url = "/etc/rspamd/custom/monitoring_nolog.map", + description = "Monitoring hosts", + type = "regexp" +} + +rspamd_config:register_symbol({ + name = 'SMTP_ACCESS', + type = 'postfilter', + callback = function(task) + local util = require("rspamd_util") + local rspamd_logger = require "rspamd_logger" + local rspamd_ip = require 'rspamd_ip' + local uname = task:get_user() + local limited_access = task:get_symbol("SMTP_LIMITED_ACCESS") + + if not uname then + return false + end + + if not limited_access then + return false + end + + local hash_key = 'SMTP_ALLOW_NETS_' .. uname + + local redis_params = rspamd_parse_redis_server('smtp_access') + local ip = task:get_from_ip() + + if ip == nil or not ip:is_valid() then + return false + end + + local from_ip_string = tostring(ip) + smtp_access_table = {from_ip_string} + + local maxbits = 128 + local minbits = 32 + if ip:get_version() == 4 then + maxbits = 32 + minbits = 8 + end + for i=maxbits,minbits,-1 do + local nip = ip:apply_mask(i):to_string() .. "/" .. i + table.insert(smtp_access_table, nip) + end + local function smtp_access_cb(err, data) + if err then + rspamd_logger.infox(rspamd_config, "smtp_access query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err) + return false + else + rspamd_logger.infox(rspamd_config, "checking ip %s for smtp_access in %s", from_ip_string, hash_key) + for k,v in pairs(data) do + if (v and v ~= userdata and v == '1') then + rspamd_logger.infox(rspamd_config, "found ip in smtp_access map") + task:insert_result(true, 'SMTP_ACCESS', 0.0, from_ip_string) + return true + end + end + rspamd_logger.infox(rspamd_config, "couldnt find ip in smtp_access map") + task:insert_result(true, 'SMTP_ACCESS', 999.0, from_ip_string) + return true + end + end + table.insert(smtp_access_table, 1, hash_key) + local redis_ret_user = rspamd_redis_make_request(task, + redis_params, -- connect params + hash_key, -- hash key + false, -- is write + smtp_access_cb, --callback + 'HMGET', -- command + smtp_access_table -- arguments + ) + if not redis_ret_user then + rspamd_logger.infox(rspamd_config, "cannot check smtp_access redis map") + end + end, + priority = 10 +}) + +rspamd_config:register_symbol({ + name = 'POSTMASTER_HANDLER', + type = 'prefilter', + callback = function(task) + local rcpts = task:get_recipients('smtp') + local rspamd_logger = require "rspamd_logger" + local lua_util = require "lua_util" + local from = task:get_from(1) + + -- not applying to mails with more than one rcpt to avoid bypassing filters by addressing postmaster + if rcpts and #rcpts == 1 then + for _,rcpt in ipairs(rcpts) do + local rcpt_split = rspamd_str_split(rcpt['addr'], '@') + if #rcpt_split == 2 then + if rcpt_split[1] == 'postmaster' then + task:set_pre_result('accept', 'whitelisting postmaster smtp rcpt') + return + end + end + end + end + + if from then + for _,fr in ipairs(from) do + local fr_split = rspamd_str_split(fr['addr'], '@') + if #fr_split == 2 then + if fr_split[1] == 'postmaster' and task:get_user() then + -- no whitelist, keep signatures + task:insert_result(true, 'POSTMASTER_FROM', -2500.0) + return + end + end + end + end + + end, + priority = 10 +}) + +rspamd_config:register_symbol({ + name = 'KEEP_SPAM', + type = 'prefilter', + callback = function(task) + local util = require("rspamd_util") + local rspamd_logger = require "rspamd_logger" + local rspamd_ip = require 'rspamd_ip' + local uname = task:get_user() + + if uname then + return false + end + + local redis_params = rspamd_parse_redis_server('keep_spam') + local ip = task:get_from_ip() + + if ip == nil or not ip:is_valid() then + return false + end + + local from_ip_string = tostring(ip) + ip_check_table = {from_ip_string} + + local maxbits = 128 + local minbits = 32 + if ip:get_version() == 4 then + maxbits = 32 + minbits = 8 + end + for i=maxbits,minbits,-1 do + local nip = ip:apply_mask(i):to_string() .. "/" .. i + table.insert(ip_check_table, nip) + end + local function keep_spam_cb(err, data) + if err then + rspamd_logger.infox(rspamd_config, "keep_spam query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err) + return false + else + for k,v in pairs(data) do + if (v and v ~= userdata and v == '1') then + rspamd_logger.infox(rspamd_config, "found ip in keep_spam map, setting pre-result") + task:set_pre_result('accept', 'ip matched with forward hosts') + end + end + end + end + table.insert(ip_check_table, 1, 'KEEP_SPAM') + local redis_ret_user = rspamd_redis_make_request(task, + redis_params, -- connect params + 'KEEP_SPAM', -- hash key + false, -- is write + keep_spam_cb, --callback + 'HMGET', -- command + ip_check_table -- arguments + ) + if not redis_ret_user then + rspamd_logger.infox(rspamd_config, "cannot check keep_spam redis map") + end + end, + priority = 19 +}) + +rspamd_config:register_symbol({ + name = 'TLS_HEADER', + type = 'postfilter', + callback = function(task) + local rspamd_logger = require "rspamd_logger" + local tls_tag = task:get_request_header('TLS-Version') + if type(tls_tag) == 'nil' then + task:set_milter_reply({ + add_headers = {['X-Last-TLS-Session-Version'] = 'None'} + }) + else + task:set_milter_reply({ + add_headers = {['X-Last-TLS-Session-Version'] = tostring(tls_tag)} + }) + end + end, + priority = 12 +}) + +rspamd_config:register_symbol({ + name = 'TAG_MOO', + type = 'postfilter', + callback = function(task) + local util = require("rspamd_util") + local rspamd_logger = require "rspamd_logger" + local redis_params = rspamd_parse_redis_server('taghandler') + local rspamd_http = require "rspamd_http" + local rcpts = task:get_recipients('smtp') + local lua_util = require "lua_util" + + local tagged_rcpt = task:get_symbol("TAGGED_RCPT") + local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN") + + local function remove_moo_tag() + local moo_tag_header = task:get_header('X-Moo-Tag', false) + if moo_tag_header then + task:set_milter_reply({ + remove_headers = {['X-Moo-Tag'] = 0}, + }) + end + return true + end + + if tagged_rcpt and tagged_rcpt[1].options and mailcow_domain then + local tag = tagged_rcpt[1].options[1] + rspamd_logger.infox("found tag: %s", tag) + local action = task:get_metric_action('default') + rspamd_logger.infox("metric action now: %s", action) + + if action ~= 'no action' and action ~= 'greylist' then + rspamd_logger.infox("skipping tag handler for action: %s", action) + remove_moo_tag() + return true + end + + local function http_callback(err_message, code, body, headers) + if body ~= nil and body ~= "" then + rspamd_logger.infox(rspamd_config, "expanding rcpt to \"%s\"", body) + + local function tag_callback_subject(err, data) + if err or type(data) ~= 'string' then + rspamd_logger.infox(rspamd_config, "subject tag handler rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying subfolder tag handler...", body, data, err) + + local function tag_callback_subfolder(err, data) + if err or type(data) ~= 'string' then + rspamd_logger.infox(rspamd_config, "subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err) + remove_moo_tag() + else + rspamd_logger.infox("Add X-Moo-Tag header") + task:set_milter_reply({ + add_headers = {['X-Moo-Tag'] = 'YES'} + }) + end + end + + local redis_ret_subfolder = rspamd_redis_make_request(task, + redis_params, -- connect params + body, -- hash key + false, -- is write + tag_callback_subfolder, --callback + 'HGET', -- command + {'RCPT_WANTS_SUBFOLDER_TAG', body} -- arguments + ) + if not redis_ret_subfolder then + rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt") + remove_moo_tag() + end + + else + rspamd_logger.infox("user wants subject modified for tagged mail") + local sbj = task:get_header('Subject') + new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?=' + task:set_milter_reply({ + remove_headers = { + ['Subject'] = 1, + ['X-Moo-Tag'] = 0 + }, + add_headers = {['Subject'] = new_sbj} + }) + end + end + + local redis_ret_subject = rspamd_redis_make_request(task, + redis_params, -- connect params + body, -- hash key + false, -- is write + tag_callback_subject, --callback + 'HGET', -- command + {'RCPT_WANTS_SUBJECT_TAG', body} -- arguments + ) + if not redis_ret_subject then + rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt") + remove_moo_tag() + end + + end + end + + if rcpts and #rcpts == 1 then + for _,rcpt in ipairs(rcpts) do + local rcpt_split = rspamd_str_split(rcpt['addr'], '@') + if #rcpt_split == 2 then + if rcpt_split[1] == 'postmaster' then + rspamd_logger.infox(rspamd_config, "not expanding postmaster alias") + remove_moo_tag() + else + rspamd_http.request({ + task=task, + url='http://nginx:8081/aliasexp.php', + body='', + callback=http_callback, + headers={Rcpt=rcpt['addr']}, + }) + end + end + end + end + else + remove_moo_tag() + end + end, + priority = 19 +}) + +rspamd_config:register_symbol({ + name = 'BCC', + type = 'postfilter', + callback = function(task) + local util = require("rspamd_util") + local rspamd_http = require "rspamd_http" + local rspamd_logger = require "rspamd_logger" + + local from_table = {} + local rcpt_table = {} + + if task:has_symbol('ENCRYPTED_CHAT') then + return -- stop + end + + local send_mail = function(task, bcc_dest) + local lua_smtp = require "lua_smtp" + local function sendmail_cb(ret, err) + if not ret then + rspamd_logger.errx(task, 'BCC SMTP ERROR: %s', err) + else + rspamd_logger.infox(rspamd_config, "BCC SMTP SUCCESS TO %s", bcc_dest) + end + end + if not bcc_dest then + return -- stop + end + -- dot stuff content before sending + local email_content = tostring(task:get_content()) + email_content = string.gsub(email_content, "\r\n%.", "\r\n..") + -- send mail + lua_smtp.sendmail({ + task = task, + host = os.getenv("IPV4_NETWORK") .. '.253', + port = 591, + from = task:get_from(stp)[1].addr, + recipients = bcc_dest, + helo = 'bcc', + timeout = 20, + }, email_content, sendmail_cb) + end + + -- determine from + local from = task:get_from('smtp') + if from then + for _, a in ipairs(from) do + table.insert(from_table, a['addr']) -- add this rcpt to table + table.insert(from_table, '@' .. a['domain']) -- add this rcpts domain to table + end + else + return -- stop + end + + -- determine rcpts + local rcpts = task:get_recipients('smtp') + if rcpts then + for _, a in ipairs(rcpts) do + table.insert(rcpt_table, a['addr']) -- add this rcpt to table + table.insert(rcpt_table, '@' .. a['domain']) -- add this rcpts domain to table + end + else + return -- stop + end + + local action = task:get_metric_action('default') + rspamd_logger.infox("metric action now: %s", action) + + local function rcpt_callback(err_message, code, body, headers) + if err_message == nil and code == 201 and body ~= nil then + if action == 'no action' or action == 'add header' or action == 'rewrite subject' then + send_mail(task, body) + end + end + end + + local function from_callback(err_message, code, body, headers) + if err_message == nil and code == 201 and body ~= nil then + if action == 'no action' or action == 'add header' or action == 'rewrite subject' then + send_mail(task, body) + end + end + end + + if rcpt_table then + for _,e in ipairs(rcpt_table) do + rspamd_logger.infox(rspamd_config, "checking bcc for rcpt address %s", e) + rspamd_http.request({ + task=task, + url='http://nginx:8081/bcc.php', + body='', + callback=rcpt_callback, + headers={Rcpt=e} + }) + end + end + + if from_table then + for _,e in ipairs(from_table) do + rspamd_logger.infox(rspamd_config, "checking bcc for from address %s", e) + rspamd_http.request({ + task=task, + url='http://nginx:8081/bcc.php', + body='', + callback=from_callback, + headers={From=e} + }) + end + end + + return true + end, + priority = 20 +}) + +rspamd_config:register_symbol({ + name = 'DYN_RL_CHECK', + type = 'prefilter', + callback = function(task) + local util = require("rspamd_util") + local redis_params = rspamd_parse_redis_server('dyn_rl') + local rspamd_logger = require "rspamd_logger" + local envfrom = task:get_from(1) + local uname = task:get_user() + if not envfrom or not uname then + return false + end + local uname = uname:lower() + + local env_from_domain = envfrom[1].domain:lower() -- get smtp from domain in lower case + + local function redis_cb_user(err, data) + + if err or type(data) ~= 'string' then + rspamd_logger.infox(rspamd_config, "dynamic ratelimit request for user %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying dynamic ratelimit for domain...", uname, data, err) + + local function redis_key_cb_domain(err, data) + if err or type(data) ~= 'string' then + rspamd_logger.infox(rspamd_config, "dynamic ratelimit request for domain %s returned invalid or empty data (\"%s\") or error (\"%s\")", env_from_domain, data, err) + else + rspamd_logger.infox(rspamd_config, "found dynamic ratelimit in redis for domain %s with value %s", env_from_domain, data) + task:insert_result('DYN_RL', 0.0, data, env_from_domain) + end + end + + local redis_ret_domain = rspamd_redis_make_request(task, + redis_params, -- connect params + env_from_domain, -- hash key + false, -- is write + redis_key_cb_domain, --callback + 'HGET', -- command + {'RL_VALUE', env_from_domain} -- arguments + ) + if not redis_ret_domain then + rspamd_logger.infox(rspamd_config, "cannot make request to load ratelimit for domain") + end + else + rspamd_logger.infox(rspamd_config, "found dynamic ratelimit in redis for user %s with value %s", uname, data) + task:insert_result('DYN_RL', 0.0, data, uname) + end + + end + + local redis_ret_user = rspamd_redis_make_request(task, + redis_params, -- connect params + uname, -- hash key + false, -- is write + redis_cb_user, --callback + 'HGET', -- command + {'RL_VALUE', uname} -- arguments + ) + if not redis_ret_user then + rspamd_logger.infox(rspamd_config, "cannot make request to load ratelimit for user") + end + return true + end, + flags = 'empty', + priority = 20 +}) + +rspamd_config:register_symbol({ + name = 'NO_LOG_STAT', + type = 'postfilter', + callback = function(task) + local from = task:get_header('From') + if from and (monitoring_hosts:get_key(from) or from == "watchdog@localhost") then + task:set_flag('no_log') + task:set_flag('no_stat') + end + end +}) + +rspamd_config:register_symbol({ + name = 'MOO_FOOTER', + type = 'prefilter', + callback = function(task) + local cjson = require "cjson" + local lua_mime = require "lua_mime" + local lua_util = require "lua_util" + local rspamd_logger = require "rspamd_logger" + local rspamd_http = require "rspamd_http" + local envfrom = task:get_from(1) + local uname = task:get_user() + if not envfrom or not uname then + return false + end + local uname = uname:lower() + local env_from_domain = envfrom[1].domain:lower() + local env_from_addr = envfrom[1].addr:lower() + + -- determine newline type + local function newline(task) + local t = task:get_newlines_type() + + if t == 'cr' then + return '\r' + elseif t == 'lf' then + return '\n' + end + + return '\r\n' + end + -- retrieve footer + local function footer_cb(err_message, code, data, headers) + if err or type(data) ~= 'string' then + rspamd_logger.infox(rspamd_config, "domain wide footer request for user %s returned invalid or empty data (\"%s\") or error (\"%s\")", uname, data, err) + else + + -- parse json string + local footer = cjson.decode(data) + if not footer then + rspamd_logger.infox(rspamd_config, "parsing domain wide footer for user %s returned invalid or empty data (\"%s\") or error (\"%s\")", uname, data, err) + else + if footer and type(footer) == "table" and (footer.html and footer.html ~= "" or footer.plain and footer.plain ~= "") then + rspamd_logger.infox(rspamd_config, "found domain wide footer for user %s: html=%s, plain=%s, vars=%s", uname, footer.html, footer.plain, footer.vars) + + if footer.skip_replies ~= 0 then + in_reply_to = task:get_header_raw('in-reply-to') + if in_reply_to then + rspamd_logger.infox(rspamd_config, "mail is a reply - skip footer") + return + end + end + + local envfrom_mime = task:get_from(2) + local from_name = "" + if envfrom_mime and envfrom_mime[1].name then + from_name = envfrom_mime[1].name + elseif envfrom and envfrom[1].name then + from_name = envfrom[1].name + end + + -- default replacements + local replacements = { + auth_user = uname, + from_user = envfrom[1].user, + from_name = from_name, + from_addr = envfrom[1].addr, + from_domain = envfrom[1].domain:lower() + } + -- add custom mailbox attributes + if footer.vars and type(footer.vars) == "string" then + local footer_vars = cjson.decode(footer.vars) + + if type(footer_vars) == "table" then + for key, value in pairs(footer_vars) do + replacements[key] = value + end + end + end + if footer.html and footer.html ~= "" then + footer.html = lua_util.jinja_template(footer.html, replacements, true) + end + if footer.plain and footer.plain ~= "" then + footer.plain = lua_util.jinja_template(footer.plain, replacements, true) + end + + -- add footer + local out = {} + local rewrite = lua_mime.add_text_footer(task, footer.html, footer.plain) or {} + + local seen_cte + local newline_s = newline(task) + + local function rewrite_ct_cb(name, hdr) + if rewrite.need_rewrite_ct then + if name:lower() == 'content-type' then + local nct = string.format('%s: %s/%s; charset=utf-8', + 'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype) + out[#out + 1] = nct + -- update Content-Type header + task:set_milter_reply({ + remove_headers = {['Content-Type'] = 0}, + }) + task:set_milter_reply({ + add_headers = {['Content-Type'] = string.format('%s/%s; charset=utf-8', rewrite.new_ct.type, rewrite.new_ct.subtype)} + }) + return + elseif name:lower() == 'content-transfer-encoding' then + out[#out + 1] = string.format('%s: %s', + 'Content-Transfer-Encoding', 'quoted-printable') + -- update Content-Transfer-Encoding header + task:set_milter_reply({ + remove_headers = {['Content-Transfer-Encoding'] = 0}, + }) + task:set_milter_reply({ + add_headers = {['Content-Transfer-Encoding'] = 'quoted-printable'} + }) + seen_cte = true + return + end + end + out[#out + 1] = hdr.raw:gsub('\r?\n?$', '') + end + + task:headers_foreach(rewrite_ct_cb, {full = true}) + + if not seen_cte and rewrite.need_rewrite_ct then + out[#out + 1] = string.format('%s: %s', 'Content-Transfer-Encoding', 'quoted-printable') + end + + -- End of headers + out[#out + 1] = newline_s + + if rewrite.out then + for _,o in ipairs(rewrite.out) do + out[#out + 1] = o + end + else + out[#out + 1] = task:get_rawbody() + end + local out_parts = {} + for _,o in ipairs(out) do + if type(o) ~= 'table' then + out_parts[#out_parts + 1] = o + out_parts[#out_parts + 1] = newline_s + else + local removePrefix = "--\x0D\x0AContent-Type" + if string.lower(string.sub(tostring(o[1]), 1, string.len(removePrefix))) == string.lower(removePrefix) then + o[1] = string.sub(tostring(o[1]), string.len("--\x0D\x0A") + 1) + end + out_parts[#out_parts + 1] = o[1] + if o[2] then + out_parts[#out_parts + 1] = newline_s + end + end + end + task:set_message(out_parts) + else + rspamd_logger.infox(rspamd_config, "domain wide footer request for user %s returned invalid or empty data (\"%s\")", uname, data) + end + end + end + end + + -- fetch footer + rspamd_http.request({ + task=task, + url='http://nginx:8081/footer.php', + body='', + callback=footer_cb, + headers={Domain=env_from_domain,Username=uname,From=env_from_addr}, + }) + + return true + end, + priority = 1 +}) diff --git a/mailcow/data/conf/rspamd/meta_exporter/pipe.php b/mailcow/data/conf/rspamd/meta_exporter/pipe.php new file mode 100644 index 0000000..4d8e2a1 --- /dev/null +++ b/mailcow/data/conf/rspamd/meta_exporter/pipe.php @@ -0,0 +1,261 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, +]; +try { + $pdo = new PDO($dsn, $database_user, $database_pass, $opt); +} +catch (PDOException $e) { + error_log("QUARANTINE: " . $e . PHP_EOL); + http_response_code(501); + exit; +} +// Init Redis +$redis = new Redis(); +$redis->connect('redis-mailcow', 6379); +$redis->auth(getenv("REDISPASS")); + +// Functions +function parse_email($email) { + if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false; + $a = strrpos($email, '@'); + return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1)); +} +if (!function_exists('getallheaders')) { + function getallheaders() { + if (!is_array($_SERVER)) { + return array(); + } + $headers = array(); + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) == 'HTTP_') { + $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + } + } + return $headers; + } +} + +$raw_data_content = file_get_contents('php://input'); +$raw_data = mb_convert_encoding($raw_data_content, 'HTML-ENTITIES', "UTF-8"); +$headers = getallheaders(); + +$qid = $headers['X-Rspamd-Qid']; +$fuzzy = $headers['X-Rspamd-Fuzzy']; +$subject = iconv_mime_decode($headers['X-Rspamd-Subject']); +$score = $headers['X-Rspamd-Score']; +$rcpts = $headers['X-Rspamd-Rcpt']; +$user = $headers['X-Rspamd-User']; +$ip = $headers['X-Rspamd-Ip']; +$action = $headers['X-Rspamd-Action']; +$sender = $headers['X-Rspamd-From']; +$symbols = $headers['X-Rspamd-Symbols']; + +$raw_size = (int)$_SERVER['CONTENT_LENGTH']; + +if (empty($sender)) { + error_log("QUARANTINE: Unknown sender, assuming empty-env-from@localhost" . PHP_EOL); + $sender = 'empty-env-from@localhost'; +} + +if ($fuzzy == 'unknown') { + $fuzzy = '[]'; +} + +try { + $max_size = (int)$redis->Get('Q_MAX_SIZE'); + if (($max_size * 1048576) < $raw_size) { + error_log(sprintf("QUARANTINE: Message too large: %d b exceeds %d b", $raw_size, ($max_size * 1048576)) . PHP_EOL); + http_response_code(505); + exit; + } + if ($exclude_domains = $redis->Get('Q_EXCLUDE_DOMAINS')) { + $exclude_domains = json_decode($exclude_domains, true); + } + $retention_size = (int)$redis->Get('Q_RETENTION_SIZE'); +} +catch (RedisException $e) { + error_log("QUARANTINE: " . $e . PHP_EOL); + http_response_code(504); + exit; +} + +$rcpt_final_mailboxes = array(); + +// Loop through all rcpts +foreach (json_decode($rcpts, true) as $rcpt) { + // Remove tag + $rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt); + + // Break rcpt into local part and domain part + $parsed_rcpt = parse_email($rcpt); + + // Skip if not a mailcow handled domain + try { + if (!$redis->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) { + continue; + } + } + catch (RedisException $e) { + error_log("QUARANTINE: " . $e . PHP_EOL); + http_response_code(504); + exit; + } + + // Skip if domain is excluded + if (in_array($parsed_rcpt['domain'], $exclude_domains)) { + error_log(sprintf("QUARANTINE: Skipped domain %s", $parsed_rcpt['domain']) . PHP_EOL); + continue; + } + + // Always assume rcpt is not a final mailbox but an alias for a mailbox or further aliases + // + // rcpt + // | + // mailbox <-- goto ---> alias1, alias2, mailbox2 + // | | + // mailbox3 | + // | + // alias3 ---> mailbox4 + // + try { + $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'"); + $stmt->execute(array( + ':rcpt' => $rcpt + )); + $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; + if (empty($gotos)) { + $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'"); + $stmt->execute(array( + ':rcpt' => '@' . $parsed_rcpt['domain'] + )); + $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; + } + if (empty($gotos)) { + $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :rcpt AND `active` = '1'"); + $stmt->execute(array(':rcpt' => $parsed_rcpt['domain'])); + $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain']; + if ($goto_branch) { + $gotos = $parsed_rcpt['local'] . '@' . $goto_branch; + } + } + $gotos_array = explode(',', $gotos); + + $loop_c = 0; + + while (count($gotos_array) != 0 && $loop_c <= 20) { + + // Loop through all found gotos + foreach ($gotos_array as $index => &$goto) { + error_log("RCPT RESOVLER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL); + $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND (`active`= '1' OR `active`= '2');"); + $stmt->execute(array(':goto' => $goto)); + $username = $stmt->fetch(PDO::FETCH_ASSOC)['username']; + if (!empty($username)) { + error_log("RCPT RESOVLER: http pipe: mailbox found: " . $username . PHP_EOL); + // Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate + if (!in_array($username, $rcpt_final_mailboxes)) { + $rcpt_final_mailboxes[] = $username; + } + } + else { + $parsed_goto = parse_email($goto); + if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) { + error_log("RCPT RESOVLER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL); + } + else { + $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'"); + $stmt->execute(array(':goto' => $goto)); + $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; + if ($goto_branch) { + error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL); + $goto_branch_array = explode(',', $goto_branch); + } else { + $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'"); + $stmt->execute(array(':domain' => $parsed_goto['domain'])); + $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain']; + if ($goto_branch) { + error_log("RCPT RESOVLER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL); + $goto_branch_array = array($parsed_goto['local'] . '@' . $goto_branch); + } + } + } + } + // goto item was processed, unset + unset($gotos_array[$index]); + } + + // Merge goto branch array derived from previous loop (if any), filter duplicates and unset goto branch array + if (!empty($goto_branch_array)) { + $gotos_array = array_unique(array_merge($gotos_array, $goto_branch_array)); + unset($goto_branch_array); + } + + // Reindex array + $gotos_array = array_values($gotos_array); + + // Force exit if loop cannot be solved + // Postfix does not allow for alias loops, so this should never happen. + $loop_c++; + error_log("RCPT RESOVLER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL); + } + } + catch (PDOException $e) { + error_log("RCPT RESOVLER: " . $e->getMessage() . PHP_EOL); + http_response_code(502); + exit; + } +} + +foreach ($rcpt_final_mailboxes as $rcpt_final) { + error_log("QUARANTINE: quarantine pipe: processing quarantine message for rcpt " . $rcpt_final . PHP_EOL); + try { + $stmt = $pdo->prepare("INSERT INTO `quarantine` (`qid`, `subject`, `score`, `sender`, `rcpt`, `symbols`, `user`, `ip`, `msg`, `action`, `fuzzy_hashes`) + VALUES (:qid, :subject, :score, :sender, :rcpt, :symbols, :user, :ip, :msg, :action, :fuzzy_hashes)"); + $stmt->execute(array( + ':qid' => $qid, + ':subject' => $subject, + ':score' => $score, + ':sender' => $sender, + ':rcpt' => $rcpt_final, + ':symbols' => $symbols, + ':user' => $user, + ':ip' => $ip, + ':msg' => $raw_data, + ':action' => $action, + ':fuzzy_hashes' => $fuzzy + )); + $stmt = $pdo->prepare('DELETE FROM `quarantine` WHERE `rcpt` = :rcpt AND `id` NOT IN ( + SELECT `id` + FROM ( + SELECT `id` + FROM `quarantine` + WHERE `rcpt` = :rcpt2 + ORDER BY id DESC + LIMIT :retention_size + ) x + );'); + $stmt->execute(array( + ':rcpt' => $rcpt_final, + ':rcpt2' => $rcpt_final, + ':retention_size' => $retention_size + )); + } + catch (PDOException $e) { + error_log("QUARANTINE: " . $e->getMessage() . PHP_EOL); + http_response_code(503); + exit; + } +} + diff --git a/mailcow/data/conf/rspamd/meta_exporter/pipe_rl.php b/mailcow/data/conf/rspamd/meta_exporter/pipe_rl.php new file mode 100644 index 0000000..f9a21ca --- /dev/null +++ b/mailcow/data/conf/rspamd/meta_exporter/pipe_rl.php @@ -0,0 +1,49 @@ +connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT')); + } + else { + $redis->connect('redis-mailcow', 6379); + } + $redis->auth(getenv("REDISPASS")); +} +catch (Exception $e) { + exit; +} + +$raw_data_content = file_get_contents('php://input'); +$raw_data_decoded = json_decode($raw_data_content, true); + +$data['time'] = time(); +$data['rcpt'] = implode(', ', $raw_data_decoded['rcpt']); +$data['from'] = $raw_data_decoded['from']; +$data['user'] = $raw_data_decoded['user']; +$symbol_rl_key = array_search('RATELIMITED', array_column($raw_data_decoded['symbols'], 'name')); +$data['rl_info'] = implode($raw_data_decoded['symbols'][$symbol_rl_key]['options']); +preg_match('/(.+)\((.+)\)/i', $data['rl_info'], $rl_matches); +if (!empty($rl_matches[1]) && !empty($rl_matches[2])) { + $data['rl_name'] = $rl_matches[1]; + $data['rl_hash'] = $rl_matches[2]; +} +else { + $data['rl_name'] = 'err'; + $data['rl_hash'] = 'err'; +} +$data['qid'] = $raw_data_decoded['qid']; +$data['ip'] = $raw_data_decoded['ip']; +$data['message_id'] = $raw_data_decoded['message_id']; +$data['header_subject'] = implode(' ', $raw_data_decoded['header_subject']); +$data['header_from'] = implode(', ', $raw_data_decoded['header_from']); + +$redis->lpush('RL_LOG', json_encode($data)); +exit; + diff --git a/mailcow/data/conf/rspamd/meta_exporter/pushover.php b/mailcow/data/conf/rspamd/meta_exporter/pushover.php new file mode 100644 index 0000000..af1b21e --- /dev/null +++ b/mailcow/data/conf/rspamd/meta_exporter/pushover.php @@ -0,0 +1,276 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, +]; +try { + $pdo = new PDO($dsn, $database_user, $database_pass, $opt); +} +catch (PDOException $e) { + error_log("NOTIFY: " . $e . PHP_EOL); + http_response_code(501); + exit; +} +// Init Redis +$redis = new Redis(); +$redis->connect('redis-mailcow', 6379); +$redis->auth(getenv("REDISPASS")); + +// Functions +function parse_email($email) { + if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false; + $a = strrpos($email, '@'); + return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1)); +} +if (!function_exists('getallheaders')) { + function getallheaders() { + if (!is_array($_SERVER)) { + return array(); + } + $headers = array(); + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) == 'HTTP_') { + $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + } + } + return $headers; + } +} + +$headers = getallheaders(); +$json_body = json_decode(file_get_contents('php://input')); + +$qid = $headers['X-Rspamd-Qid']; +$rcpts = $headers['X-Rspamd-Rcpt']; +$sender = $headers['X-Rspamd-From']; +$ip = $headers['X-Rspamd-Ip']; +$subject = iconv_mime_decode($headers['X-Rspamd-Subject']); +$messageid= $json_body->message_id; +$priority = 0; + +$symbols_array = json_decode($headers['X-Rspamd-Symbols'], true); +if (is_array($symbols_array)) { + foreach ($symbols_array as $symbol) { + if ($symbol['name'] == 'HAS_X_PRIO_ONE') { + $priority = 1; + break; + } + } +} + +$sender_address = $json_body->header_from[0]; +$sender_name = '-'; +if (preg_match('/(?.*?)<(?
.*?)>/i', $sender_address, $matches)) { + $sender_address = $matches['address']; + $sender_name = trim($matches['name'], '"\' '); +} + +$to_address = $json_body->header_to[0]; +$to_name = '-'; +if (preg_match('/(?.*?)<(?
.*?)>/i', $to_address, $matches)) { + $to_address = $matches['address']; + $to_name = trim($matches['name'], '"\' '); +} + +$rcpt_final_mailboxes = array(); + +// Loop through all rcpts +foreach (json_decode($rcpts, true) as $rcpt) { + // Remove tag + $rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt); + + // Break rcpt into local part and domain part + $parsed_rcpt = parse_email($rcpt); + + // Skip if not a mailcow handled domain + try { + if (!$redis->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) { + continue; + } + } + catch (RedisException $e) { + error_log("NOTIFY: " . $e . PHP_EOL); + http_response_code(504); + exit; + } + + // Always assume rcpt is not a final mailbox but an alias for a mailbox or further aliases + // + // rcpt + // | + // mailbox <-- goto ---> alias1, alias2, mailbox2 + // | | + // mailbox3 | + // | + // alias3 ---> mailbox4 + // + try { + $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'"); + $stmt->execute(array( + ':rcpt' => $rcpt + )); + $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; + if (empty($gotos)) { + $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :rcpt AND `active` = '1'"); + $stmt->execute(array( + ':rcpt' => '@' . $parsed_rcpt['domain'] + )); + $gotos = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; + } + if (empty($gotos)) { + $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :rcpt AND `active` = '1'"); + $stmt->execute(array(':rcpt' => $parsed_rcpt['domain'])); + $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain']; + if ($goto_branch) { + $gotos = $parsed_rcpt['local'] . '@' . $goto_branch; + } + } + $gotos_array = explode(',', $gotos); + + $loop_c = 0; + + while (count($gotos_array) != 0 && $loop_c <= 20) { + + // Loop through all found gotos + foreach ($gotos_array as $index => &$goto) { + error_log("RCPT RESOVLER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL); + $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND (`active`= '1' OR `active`= '2');"); + $stmt->execute(array(':goto' => $goto)); + $username = $stmt->fetch(PDO::FETCH_ASSOC)['username']; + if (!empty($username)) { + error_log("RCPT RESOVLER: http pipe: mailbox found: " . $username . PHP_EOL); + // Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate + if (!in_array($username, $rcpt_final_mailboxes)) { + $rcpt_final_mailboxes[] = $username; + } + } + else { + $parsed_goto = parse_email($goto); + if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) { + error_log("RCPT RESOVLER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL); + } + else { + $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'"); + $stmt->execute(array(':goto' => $goto)); + $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto']; + if ($goto_branch) { + error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL); + $goto_branch_array = explode(',', $goto_branch); + } else { + $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'"); + $stmt->execute(array(':domain' => $parsed_goto['domain'])); + $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain']; + if ($goto_branch) { + error_log("RCPT RESOVLER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL); + $goto_branch_array = array($parsed_goto['local'] . '@' . $goto_branch); + } + } + } + } + // goto item was processed, unset + unset($gotos_array[$index]); + } + + // Merge goto branch array derived from previous loop (if any), filter duplicates and unset goto branch array + if (!empty($goto_branch_array)) { + $gotos_array = array_unique(array_merge($gotos_array, $goto_branch_array)); + unset($goto_branch_array); + } + + // Reindex array + $gotos_array = array_values($gotos_array); + + // Force exit if loop cannot be solved + // Postfix does not allow for alias loops, so this should never happen. + $loop_c++; + error_log("RCPT RESOVLER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL); + } + } + catch (PDOException $e) { + error_log("RCPT RESOVLER: " . $e->getMessage() . PHP_EOL); + http_response_code(502); + exit; + } +} + + +foreach ($rcpt_final_mailboxes as $rcpt_final) { + error_log("NOTIFY: pushover pipe: processing pushover message for rcpt " . $rcpt_final . PHP_EOL); + $stmt = $pdo->prepare("SELECT * FROM `pushover` + WHERE `username` = :username AND `active` = '1'"); + $stmt->execute(array( + ':username' => $rcpt_final + )); + $api_data = $stmt->fetch(PDO::FETCH_ASSOC); + if (isset($api_data['key']) && isset($api_data['token'])) { + $title = (!empty($api_data['title'])) ? $api_data['title'] : 'Mail'; + $text = (!empty($api_data['text'])) ? $api_data['text'] : 'You\'ve got mail 📧'; + $attributes = json_decode($api_data['attributes'], true); + $senders = explode(',', $api_data['senders']); + $senders = array_filter($senders); + $senders_regex = $api_data['senders_regex']; + $sender_validated = false; + if (empty($senders) && empty($senders_regex)) { + $sender_validated = true; + } + else { + if (!empty($senders)) { + if (in_array($sender, $senders)) { + $sender_validated = true; + } + } + if (!empty($senders_regex) && $sender_validated !== true) { + if (preg_match($senders_regex, $sender)) { + $sender_validated = true; + } + } + } + if ($sender_validated === false) { + error_log("NOTIFY: pushover pipe: skipping unwanted sender " . $sender); + continue; + } + if ($attributes['only_x_prio'] == "1" && $priority == 0) { + error_log("NOTIFY: pushover pipe: mail has no X-Priority: 1 header, skipping"); + continue; + } + $post_fields = array( + "token" => $api_data['token'], + "user" => $api_data['key'], + "title" => sprintf("%s", str_replace( + array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}', '{TO_NAME}', '{TO_ADDRESS}', '{MSG_ID}'), + array($subject, $sender, $sender_name, $sender_address, $to_name, $to_address, $messageid), $title) + ), + "priority" => $priority, + "message" => sprintf("%s", str_replace( + array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}', '{TO_NAME}', '{TO_ADDRESS}', '{MSG_ID}', '\n'), + array($subject, $sender, $sender_name, $sender_address, $to_name, $to_address, $messageid, PHP_EOL), $text) + ), + "sound" => $attributes['sound'] ?? "pushover" + ); + if ($attributes['evaluate_x_prio'] == "1" && $priority == 1) { + $post_fields['expire'] = 600; + $post_fields['retry'] = 120; + $post_fields['priority'] = 2; + } + curl_setopt_array($ch = curl_init(), array( + CURLOPT_URL => "https://api.pushover.net/1/messages.json", + CURLOPT_POSTFIELDS => $post_fields, + CURLOPT_SAFE_UPLOAD => true, + CURLOPT_RETURNTRANSFER => true, + )); + $result = curl_exec($ch); + $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + error_log("NOTIFY: result: " . $httpcode . PHP_EOL); + } +} diff --git a/mailcow/data/conf/rspamd/meta_exporter/vars.inc.php b/mailcow/data/conf/rspamd/meta_exporter/vars.inc.php new file mode 100644 index 0000000..79566b0 --- /dev/null +++ b/mailcow/data/conf/rspamd/meta_exporter/vars.inc.php @@ -0,0 +1,6 @@ + diff --git a/mailcow/data/conf/rspamd/plugins.d/README.md b/mailcow/data/conf/rspamd/plugins.d/README.md new file mode 100644 index 0000000..1516cf2 --- /dev/null +++ b/mailcow/data/conf/rspamd/plugins.d/README.md @@ -0,0 +1 @@ +This is where you should copy any rspamd custom module diff --git a/mailcow/data/conf/rspamd/rspamd.conf.local b/mailcow/data/conf/rspamd/rspamd.conf.local new file mode 100644 index 0000000..9f2f8f1 --- /dev/null +++ b/mailcow/data/conf/rspamd/rspamd.conf.local @@ -0,0 +1 @@ +# rspamd.conf.local diff --git a/mailcow/data/conf/rspamd/rspamd.conf.override b/mailcow/data/conf/rspamd/rspamd.conf.override new file mode 100644 index 0000000..d033e8e --- /dev/null +++ b/mailcow/data/conf/rspamd/rspamd.conf.override @@ -0,0 +1,2 @@ +# rspamd.conf.override + diff --git a/mailcow/data/conf/sogo/custom-favicon.ico b/mailcow/data/conf/sogo/custom-favicon.ico new file mode 100644 index 0000000..4d5cb32 Binary files /dev/null and b/mailcow/data/conf/sogo/custom-favicon.ico differ diff --git a/mailcow/data/conf/sogo/custom-sogo.js b/mailcow/data/conf/sogo/custom-sogo.js new file mode 100644 index 0000000..e1f27e8 --- /dev/null +++ b/mailcow/data/conf/sogo/custom-sogo.js @@ -0,0 +1,16 @@ +// redirect to mailcow login form +document.addEventListener('DOMContentLoaded', function () { + var loginForm = document.forms.namedItem("loginForm"); + if (loginForm) { + window.location.href = '/user'; + } +}); + +// Custom SOGo JS + +// Change the visible font-size in the editor, this does not change the font of a html message by default +CKEDITOR.addCss("body {font-size: 16px !important}"); + +// Enable scayt by default +//CKEDITOR.config.scayt_autoStartup = true; + diff --git a/mailcow/data/conf/sogo/sogo.conf b/mailcow/data/conf/sogo/sogo.conf new file mode 100644 index 0000000..8455f0c --- /dev/null +++ b/mailcow/data/conf/sogo/sogo.conf @@ -0,0 +1,101 @@ +{ + SOGoCalendarDefaultRoles = ( + PublicViewer, + ConfidentialDAndTViewer, + PrivateDAndTViewer + ); + + WOWorkersCount = "20"; + SOGoACLsSendEMailNotifications = YES; + SOGoAppointmentSendEMailNotifications = YES; + SOGoDraftsFolderName = "Drafts"; + SOGoJunkFolderName= "Junk"; + SOGoMailDomain = "sogo.local"; + SOGoEnableEMailAlarms = YES; + SOGoMailHideInlineAttachments = YES; + SOGoFoldersSendEMailNotifications = YES; + SOGoForwardEnabled = YES; + + // Fixes "MODIFICATION_FAILED" error (HTTP 412) in Clients when accepting invitations from external services + SOGoDisableOrganizerEventCheck = YES; + + // Option to set Users as admin to globally manage calendar permissions etc. Disabled by default + // SOGoSuperUsernames = ("moo@example.com"); + + SOGoUIAdditionalJSFiles = ( + js/theme.js, + js/custom-sogo.js + ); + + SOGoEnablePublicAccess = YES; + + // Multi-domain setup + // Domains are isolated, you can define visibility options here. + // Example: + + // SOGoDomainsVisibility = ( + // (domain1.tld, domain5.tld), + // (domain3.tld, domain2.tld) + // ); + + // self-signed is not trusted anymore + WOPort = "0.0.0.0:20000"; + SOGoMemcachedHost = "memcached"; + + SOGoLanguage = English; + SOGoMailAuxiliaryUserAccountsEnabled = YES; + // SOGoCreateIdentitiesDisabled = NO; + SOGoMailCustomFromEnabled = YES; + SOGoMailingMechanism = smtp; + SOGoSMTPAuthenticationType = plain; + + SxVMemLimit = 384; + + SOGoMaximumPingInterval = 3540; + + SOGoInternalSyncInterval = 45; + SOGoMaximumSyncInterval = 3540; + + // 100 seems to break some Android clients + //SOGoMaximumSyncWindowSize = 99; + // This should do the trick for Outlook 2016 + SOGoMaximumSyncResponseSize = 512; + + WOWatchDogRequestTimeout = 30; + WOListenQueueSize = 16; + WONoDetach = YES; + + SOGoIMAPAclConformsToIMAPExt = Yes; + SOGoPageTitle = "SOGo Groupware"; + SOGoFirstDayOfWeek = "1"; + + SOGoSieveFolderEncoding = "UTF-8"; + SOGoPasswordChangeEnabled = NO; + SOGoSentFolderName = "Sent"; + SOGoMailShowSubscribedFoldersOnly = NO; + NGImap4ConnectionStringSeparator = "/"; + SOGoSieveScriptsEnabled = YES; + SOGoTrashFolderName = "Trash"; + SOGoVacationEnabled = YES; + + SOGoCacheCleanupInterval = 900; + SOGoMaximumFailedLoginCount = 10; + SOGoMaximumFailedLoginInterval = 900; + SOGoFailedLoginBlockInterval = 900; + + GCSChannelCollectionTimer = 60; + GCSChannelExpireAge = 60; + + MySQL4Encoding = "utf8mb4"; + //SOGoDebugRequests = YES; + //SoDebugBaseURL = YES; + //ImapDebugEnabled = YES; + //SOGoEASDebugEnabled = YES; + SOGoEASSearchInBody = YES; // Experimental. Enabled since 2023-10 + //LDAPDebugEnabled = YES; + //PGDebugEnabled = YES; + //MySQL4DebugEnabled = YES; + //SOGoUIxDebugEnabled = YES; + //WODontZipResponse = YES; + WOLogFile = "/dev/sogo_log"; +} diff --git a/mailcow/data/conf/unbound/unbound.conf b/mailcow/data/conf/unbound/unbound.conf new file mode 100644 index 0000000..27110c0 --- /dev/null +++ b/mailcow/data/conf/unbound/unbound.conf @@ -0,0 +1,45 @@ +server: + verbosity: 1 + interface: 0.0.0.0 + interface: ::0 + logfile: /dev/console + do-ip4: yes + do-ip6: yes + do-udp: yes + do-tcp: yes + do-daemonize: no + #access-control: 0.0.0.0/0 allow + access-control: 10.0.0.0/8 allow + access-control: 172.16.0.0/12 allow + access-control: 192.168.0.0/16 allow + access-control: fc00::/7 allow + access-control: fe80::/10 allow + #access-control: ::0/0 allow + directory: "/etc/unbound" + username: unbound + auto-trust-anchor-file: trusted-key.key + #private-address: 10.0.0.0/8 + #private-address: 172.16.0.0/12 + #private-address: 192.168.0.0/16 + #private-address: 169.254.0.0/16 + #private-address: fc00::/7 + #private-address: fe80::/10 + # cache-min-ttl needs to be less or equal to cache-max-negative-ttl + cache-min-ttl: 5 + cache-max-negative-ttl: 60 + root-hints: "/etc/unbound/root.hints" + hide-identity: yes + hide-version: yes + max-udp-size: 4096 + msg-buffer-size: 65552 + unwanted-reply-threshold: 10000 + ipsecmod-enabled: no + +remote-control: + control-enable: yes + control-interface: 127.0.0.1 + control-port: 8953 + server-key-file: "/etc/unbound/unbound_server.key" + server-cert-file: "/etc/unbound/unbound_server.pem" + control-key-file: "/etc/unbound/unbound_control.key" + control-cert-file: "/etc/unbound/unbound_control.pem" diff --git a/mailcow/data/hooks/README.md b/mailcow/data/hooks/README.md new file mode 100644 index 0000000..d0996a1 --- /dev/null +++ b/mailcow/data/hooks/README.md @@ -0,0 +1,3 @@ +Place executable scripts in each directory to run a hook at the end of a docker-entrypoint.sh script. + +Only images with a docker-entrypoint are covered. diff --git a/mailcow/data/web/_rspamderror.php b/mailcow/data/web/_rspamderror.php new file mode 100644 index 0000000..4976e0b --- /dev/null +++ b/mailcow/data/web/_rspamderror.php @@ -0,0 +1,18 @@ +connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT')); + } + else { + $redis->connect('redis-mailcow', 6379); + } + $redis->auth(getenv("REDISPASS")); +} +catch (Exception $e) { + exit; +} +header('Content-Type: application/json'); +echo '{"error":"Unauthorized"}'; +error_log("Rspamd UI: Invalid password by " . $_SERVER['REMOTE_ADDR']); +$redis->publish("F2B_CHANNEL", "Rspamd UI: Invalid password by " . $_SERVER['REMOTE_ADDR']); diff --git a/mailcow/data/web/_status.502.html b/mailcow/data/web/_status.502.html new file mode 100644 index 0000000..c8a9b70 --- /dev/null +++ b/mailcow/data/web/_status.502.html @@ -0,0 +1,25 @@ + + + + Preparing + + + +

What is happening?

+

Please do not stop the stack while we are initializing the database or do other preparations.

+

What is happening? - Nginx cannot connect to an upstream server or other services are not ready yet.
+ This is fine if mailcow was just + installed or updated and can take a few minutes to complete.
+ Please check the logs or contact support if the error persists.

+

Quick debugging

+

Check Nginx and PHP logs:

+
docker compose logs --tail=200 php-fpm-mailcow nginx-mailcow
+

Make sure your SQL credentials in mailcow.conf (a link to .env) do fit your initialized SQL volume. If you see an access denied, you might have the wrong mailcow.conf:

+
source mailcow.conf ; docker compose exec mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME}
+

In case of a previous failed installation, create a backup of your existing data, followed by removing all volumes and starting over (NEVER do this with a production system, it will remove ALL data):

+
BACKUP_LOCATION=/tmp/ ./helper-scripts/backup_and_restore.sh backup all
+
docker compose down --volumes ; docker compose up -d
+

Make sure your timezone is correct. Use "America/New_York" for example, do not use spaces. Check here for a list.

+
Click to learn more about getting support. + + diff --git a/mailcow/data/web/admin/dashboard.php b/mailcow/data/web/admin/dashboard.php new file mode 100644 index 0000000..473f28d --- /dev/null +++ b/mailcow/data/web/admin/dashboard.php @@ -0,0 +1,89 @@ +Get('LICENSE_STATUS_CACHE')) { + $_SESSION['gal'] = json_decode($license_cache, true); +} + +$js_minifier->add('/web/js/site/dashboard.js'); + +// vmail df +$exec_fields = array('cmd' => 'system', 'task' => 'df', 'dir' => '/var/vmail'); +$vmail_df = explode(',', (string)json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields), true)); + +// containers +$containers_info = (array) docker('info'); +if ($clamd_status === false) unset($containers_info['clamd-mailcow']); +ksort($containers_info); +$containers = array(); +foreach ($containers_info as $container => $container_info) { + if (!isset($container_info['State']) || !is_array($container_info['State']) || !isset($container_info['State']['StartedAt'])){ + continue; + } + date_default_timezone_set('UTC'); + $StartedAt = date_parse($container_info['State']['StartedAt']); + if ($StartedAt['hour'] !== false) { + $date = new \DateTime(); + $date->setTimestamp(mktime( + $StartedAt['hour'], + $StartedAt['minute'], + $StartedAt['second'], + $StartedAt['month'], + $StartedAt['day'], + $StartedAt['year'])); + try { + $user_tz = new DateTimeZone(getenv('TZ')); + $date->setTimezone($user_tz); + $container_info['State']['StartedAtHR'] = $date->format('r'); + } catch(Exception $e) { + $container_info['State']['StartedAtHR'] = '?'; + } + } + else { + $container_info['State']['StartedAtHR'] = '?'; + } + $containers[$container] = $container_info; +} + +// get mailcow data +$hostname = getenv('MAILCOW_HOSTNAME'); +$timezone = getenv('TZ'); + +$template = 'dashboard.twig'; +$template_data = [ + 'log_lines' => getenv('LOG_LINES'), + 'vmail_df' => $vmail_df, + 'hostname' => $hostname, + 'timezone' => $timezone, + 'gal' => @$_SESSION['gal'], + 'license_guid' => license('guid'), + 'clamd_status' => $clamd_status, + 'containers' => $containers, + 'ip_check' => customize('get', 'ip_check'), + 'lang_admin' => json_encode($lang['admin']), + 'lang_debug' => json_encode($lang['debug']), + 'lang_datatables' => json_encode($lang['datatables']), +]; + +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php'; + + diff --git a/mailcow/data/web/admin/index.php b/mailcow/data/web/admin/index.php new file mode 100644 index 0000000..05ba703 --- /dev/null +++ b/mailcow/data/web/admin/index.php @@ -0,0 +1,29 @@ + @$_SESSION['ldelay'] +]; + +$js_minifier->add('/web/js/site/index.js'); +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php'; diff --git a/mailcow/data/web/admin/mailbox.php b/mailcow/data/web/admin/mailbox.php new file mode 100644 index 0000000..d0073bb --- /dev/null +++ b/mailcow/data/web/admin/mailbox.php @@ -0,0 +1,58 @@ +add('/web/js/site/mailbox.js'); +$js_minifier->add('/web/js/presets/sieveMailbox.js'); +$js_minifier->add('/web/js/site/pwgen.js'); + +$role = "admin"; +$is_dual = (!empty($_SESSION["dual-login"]["username"])) ? 'true' : 'false'; +$allow_admin_email_login = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["ALLOW_ADMIN_EMAIL_LOGIN"])) ? 'true' : 'false'; + +// domains +$domains = mailbox('get', 'domains'); + +// mailboxes +$mailboxes = []; +foreach ($domains as $domain) { + foreach (mailbox('get', 'mailboxes', $domain) as $mailbox) { + $mailboxes[] = $mailbox; + } +} + +$template = 'mailbox.twig'; +$template_data = [ + 'acl' => $_SESSION['acl'], + 'acl_json' => json_encode($_SESSION['acl']), + 'role' => $role, + 'is_dual' => $is_dual, + 'allow_admin_email_login' => $allow_admin_email_login, + 'global_filters' => mailbox('get', 'global_filter_details'), + 'domains' => $domains, + 'mailboxes' => $mailboxes, + 'lang_mailbox' => json_encode($lang['mailbox']), + 'lang_rl' => json_encode($lang['ratelimit']), + 'lang_edit' => json_encode($lang['edit']), + 'lang_datatables' => json_encode($lang['datatables']), +]; + +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php'; diff --git a/mailcow/data/web/admin/queue.php b/mailcow/data/web/admin/queue.php new file mode 100644 index 0000000..85ec594 --- /dev/null +++ b/mailcow/data/web/admin/queue.php @@ -0,0 +1,35 @@ +add('/web/js/site/queue.js'); +$_SESSION['return_to'] = $_SERVER['REQUEST_URI']; + +$role = "admin"; + +$template = 'queue.twig'; +$template_data = [ + 'acl' => $_SESSION['acl'], + 'acl_json' => json_encode($_SESSION['acl']), + 'role' => $role, + 'lang_admin' => json_encode($lang['admin']), + 'lang_queue' => json_encode($lang['queue']), + 'lang_datatables' => json_encode($lang['datatables']) +]; + +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php'; diff --git a/mailcow/data/web/admin/system.php b/mailcow/data/web/admin/system.php new file mode 100644 index 0000000..c21d43f --- /dev/null +++ b/mailcow/data/web/admin/system.php @@ -0,0 +1,138 @@ + "get_friendly_names")); + +$js_minifier->add('/web/js/site/admin.js'); +$js_minifier->add('/web/js/presets/rspamd.js'); +$js_minifier->add('/web/js/site/pwgen.js'); + +// all domains +$domains = mailbox('get', 'domains'); +$all_domains = array_merge($domains, mailbox('get', 'alias_domains')); + +// mailboxes +$mailboxes = []; +foreach ($all_domains as $domain) { + foreach (mailbox('get', 'mailboxes', $domain) as $mailbox) { + $mailboxes[] = $mailbox; + } +} +$mailboxes = array_filter($mailboxes); + +// DKIM domains +$dkim_domains = []; +$dkim_domains_with_keys = []; +foreach($domains as $domain) { + $dkim_domains[$domain] = ['dkim' => null, 'alias_domains' => []]; + if (!empty($dkim = dkim('details', $domain))) { + $dkim_domains_with_keys[] = $domain; + if ($GLOBALS['SHOW_DKIM_PRIV_KEYS'] !== true) { + $dkim['privkey'] = base64_encode('Please set $SHOW_DKIM_PRIV_KEYS to true to show DKIM private keys.'); + } + $dkim_domains[$domain]['dkim'] = $dkim; + } + + // get alias domains + foreach (mailbox('get', 'alias_domains', $domain) as $alias_domain) { + $dkim_domains[$domain]['alias_domains'][$alias_domain] = ['dkim' => null]; + if (!empty($dkim = dkim('details', $alias_domain))) { + $dkim_domains_with_keys[] = $alias_domain; + if ($GLOBALS['SHOW_DKIM_PRIV_KEYS'] !== true) { + $dkim['privkey'] = base64_encode('Please set $SHOW_DKIM_PRIV_KEYS to true to show DKIM private keys.'); + } + $dkim_domains[$domain]['alias_domains'][$alias_domain]['dkim'] = $dkim; + } + } +} +$dkim_blind_domains = []; +foreach(dkim('blind') as $blind) { + $dkim_blind_domains[$blind] = ['dkim' => null]; + if (!empty($dkim = dkim('details', $blind))) { + $dkim_domains_with_keys[] = $blind; + if ($GLOBALS['SHOW_DKIM_PRIV_KEYS'] !== true) { + $dkim['privkey'] = base64_encode('Please set $SHOW_DKIM_PRIV_KEYS to true to show DKIM private keys.'); + } + $dkim_blind_domains[$blind]['dkim'] = $dkim; + } +} + +// rsettings +$rsettings = array_map(function ($rsetting){ + $rsetting['details'] = rsettings('details', $rsetting['id']); + return $rsetting; +}, rsettings('get')); + +// rspamd regex maps +$rspamd_regex_maps = []; +foreach ($RSPAMD_MAPS['regex'] as $rspamd_regex_desc => $rspamd_regex_map) { + $rspamd_regex_maps[$rspamd_regex_desc] = [ + 'map' => $rspamd_regex_map, + 'data' => file_get_contents('/rspamd_custom_maps/' . $rspamd_regex_map) + ]; +} + +// cors settings +$cors_settings = cors('get'); +$cors_settings['allowed_origins'] = str_replace(", ", "\n", $cors_settings['allowed_origins']); +$cors_settings['allowed_methods'] = explode(", ", $cors_settings['allowed_methods']); + +$f2b_data = fail2ban('get'); +// mbox templates +$mbox_templates = mailbox('get', 'mailbox_templates'); + +$template = 'admin.twig'; +$template_data = [ + 'tfa_data' => $tfa_data, + 'tfa_id' => @$_SESSION['tfa_id'], + 'fido2_cid' => @$_SESSION['fido2_cid'], + 'fido2_data' => $fido2_data, + 'api' => [ + 'ro' => admin_api('ro', 'get'), + 'rw' => admin_api('rw', 'get'), + ], + 'dkim_domains' => $dkim_domains, + 'dkim_domains_with_keys' => $dkim_domains_with_keys, + 'dkim_blind_domains' => $dkim_blind_domains, + 'domains' => $domains, + 'all_domains' => $all_domains, + 'mailboxes' => $mailboxes, + 'f2b_data' => $f2b_data, + 'f2b_banlist_url' => getBaseUrl() . "/f2b-banlist?id=" . $f2b_data['banlist_id'], + 'q_data' => quarantine('settings'), + 'qn_data' => quota_notification('get'), + 'pw_reset_data' => reset_password('get_notification'), + 'rsettings_map' => file_get_contents('http://nginx:8081/settings.php'), + 'rsettings' => $rsettings, + 'rspamd_regex_maps' => $rspamd_regex_maps, + 'logo_specs' => customize('get', 'main_logo_specs'), + 'logo_dark_specs' => customize('get', 'main_logo_dark_specs'), + 'ip_check' => customize('get', 'ip_check'), + 'password_complexity' => password_complexity('get'), + 'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'], + 'cors_settings' => $cors_settings, + 'is_https' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on', + 'iam_settings' => $iam_settings, + 'mbox_templates' => $mbox_templates, + 'lang_admin' => json_encode($lang['admin']), + 'lang_datatables' => json_encode($lang['datatables']) +]; + +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php'; diff --git a/mailcow/data/web/api/favicon-16x16.png b/mailcow/data/web/api/favicon-16x16.png new file mode 100644 index 0000000..8b194e6 Binary files /dev/null and b/mailcow/data/web/api/favicon-16x16.png differ diff --git a/mailcow/data/web/api/favicon-32x32.png b/mailcow/data/web/api/favicon-32x32.png new file mode 100644 index 0000000..249737f Binary files /dev/null and b/mailcow/data/web/api/favicon-32x32.png differ diff --git a/mailcow/data/web/api/index.css b/mailcow/data/web/api/index.css new file mode 100644 index 0000000..f2376fd --- /dev/null +++ b/mailcow/data/web/api/index.css @@ -0,0 +1,16 @@ +html { + box-sizing: border-box; + overflow: -moz-scrollbars-vertical; + overflow-y: scroll; +} + +*, +*:before, +*:after { + box-sizing: inherit; +} + +body { + margin: 0; + background: #fafafa; +} diff --git a/mailcow/data/web/api/index.html b/mailcow/data/web/api/index.html new file mode 100644 index 0000000..84ae62d --- /dev/null +++ b/mailcow/data/web/api/index.html @@ -0,0 +1,19 @@ + + + + + + Swagger UI + + + + + + + +
+ + + + + diff --git a/mailcow/data/web/api/oauth2-redirect.html b/mailcow/data/web/api/oauth2-redirect.html new file mode 100644 index 0000000..5640917 --- /dev/null +++ b/mailcow/data/web/api/oauth2-redirect.html @@ -0,0 +1,79 @@ + + + + Swagger UI: OAuth2 Redirect + + + + + diff --git a/mailcow/data/web/api/openapi.yaml b/mailcow/data/web/api/openapi.yaml new file mode 100644 index 0000000..afab5fb --- /dev/null +++ b/mailcow/data/web/api/openapi.yaml @@ -0,0 +1,6090 @@ +openapi: 3.1.0 +info: + description: >- + mailcow is complete e-mailing solution with advanced antispam, antivirus, + nice UI and API. + + + In order to use this API you have to create a API key and add your IP + address to the whitelist of allowed IPs this can be done by logging into the + Mailcow UI using your admin account, then go to Configuration > Access > + Edit administrator details > API. There you will find a collapsed API menu. + + + There are two types of API keys + - The read only key can only be used for all get endpoints + - The read write key can be used for all endpoints + + title: mailcow API + version: "1.0.0" + +servers: + - url: / + +components: + securitySchemes: + ApiKeyAuth: + type: apiKey + in: header + name: X-API-Key + responses: + Unauthorized: + description: Unauthorized + content: + application/json: + schema: + type: object + properties: + type: + type: string + example: error + msg: + type: string + example: authentication failed + required: + - type + - msg + +security: + - ApiKeyAuth: [] + +paths: + /api/v1/add/alias: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - mailbox + - add + - alias + - active: "1" + address: alias@domain.tld + goto: destination@domain.tld + - null + msg: + - alias_added + - alias@domain.tld + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Aliases + description: >- + You may create your own mailbox alias using this action. It takes a JSON + object containing a domain informations. + + Only one `goto*` option can be used, for ex. if you want learn as spam, + then send just `goto_spam = 1` in request body. + operationId: Create alias + requestBody: + content: + application/json: + schema: + example: + active: "1" + address: alias@domain.tld + goto: destination@domain.tld + properties: + active: + description: is alias active or not + type: boolean + address: + description: 'alias address, for catchall use "@domain.tld"' + type: string + goto: + description: "destination address, comma separated" + type: string + goto_ham: + description: learn as ham + type: boolean + goto_null: + description: silently ignore + type: boolean + goto_spam: + description: learn as spam + type: boolean + sogo_visible: + description: toggle visibility as selectable sender in SOGo + type: boolean + type: object + summary: Create alias + /api/v1/add/time_limited_alias: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - mailbox + - add + - time_limited_alias + - address: info@domain.tld + domain: domain.tld + - null + msg: + - mailbox_modified + - info@domain.tld + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Aliases + description: >- + You may create a time limited alias using this action. It takes a JSON + object containing a domain and mailbox informations. + Mailcow will generate a random alias. + operationId: Create time limited alias + requestBody: + content: + application/json: + schema: + example: + username: info@domain.tld + domain: domain.tld + properties: + username: + description: 'the mailbox an alias should be created for' + type: string + domain: + description: "the domain" + type: string + type: object + summary: Create time limited alias + /api/v1/add/app-passwd: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - app_passwd + - add + - active: "1" + username: info@domain.tld + app_name: wordpress + app_passwd: keyleudecticidechothistishownsan31 + app_passwd2: keyleudecticidechothistishownsan31 + protocols: + - imap_access + - dav_access + - smtp_access + - eas_access + - pop3_access + - sieve_access + msg: app_passwd_added + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - App Passwords + description: >- + Using this endpoint you can create a new app password for a specific + mailbox. + operationId: Create App Password + requestBody: + content: + application/json: + schema: + example: + active: "1" + username: info@domain.tld + app_name: wordpress + app_passwd: keyleudecticidechothistishownsan31 + app_passwd2: keyleudecticidechothistishownsan31 + protocols: + - imap_access + - dav_access + - smtp_access + - eas_access + - pop3_access + - sieve_access + properties: + active: + description: is alias active or not + type: boolean + username: + description: mailbox for which the app password should be created + type: string + app_name: + description: name of your app password + type: string + app_passwd: + description: your app password + type: string + app_passwd2: + description: your app password + type: string + type: object + summary: Create App Password + /api/v1/add/bcc: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - bcc + - add + - active: "1" + bcc_dest: bcc@awesomecow.tld + local_dest: mailcow.tld + type: sender + - null + msg: bcc_saved + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Address Rewriting + description: >- + Using this endpoint you can create a BCC map to forward all mails via a + bcc for a given domain. + operationId: Create BCC Map + requestBody: + content: + application/json: + schema: + example: + active: "1" + bcc_dest: bcc@awesomecow.tld + local_dest: mailcow.tld + type: sender + properties: + active: + description: 1 for a active user account 0 for a disabled user account + type: number + bcc_dest: + description: the email address where all mails should be send to + type: string + local_dest: + description: the domain which emails should be forwarded + type: string + type: + description: the type of bcc map can be `sender` or `rcpt` + enum: [sender, rcpt] + type: string + type: object + summary: Create BCC Map + /api/v1/add/dkim: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - dkim + - add + - dkim_selector: dkim + domains: hanspeterlol.de + key_size: "2048" + msg: + - dkim_added + - hanspeterlol.de + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - DKIM + description: Using this endpoint you can generate new DKIM keys. + operationId: Generate DKIM Key + requestBody: + content: + application/json: + schema: + example: + dkim_selector: dkim + domains: mailcow.tld + key_size: "2048" + properties: + dkim_selector: + description: the DKIM selector default dkim + type: string + domains: + description: a list of domains for which a dkim key should be generated + type: string + key_size: + description: the key size (1024, 2048, 3072 or 4096) + type: number + type: object + summary: Generate DKIM Key + /api/v1/add/dkim_duplicate: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - dkim + - duplicate + - from_domain: mailcow.tld + to_domain: awesomecow.tld + msg: + - dkim_duplicated + - mailcow.tld + - awesomecow.tld + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - DKIM + description: Using this endpoint you can duplicate the DKIM Key of one domain. + operationId: Duplicate DKIM Key + requestBody: + content: + application/json: + schema: + example: + from_domain: mailcow.tld + to_domain: awesomecow.tld + properties: + fron_domain: + description: the domain where the dkim key should be copied from + type: string + to_domain: + description: the domain where the dkim key should be copied to + type: string + type: object + summary: Duplicate DKIM Key + /api/v1/add/domain: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - ratelimit + - edit + - domain + - object: domain.tld + rl_frame: s + rl_value: "10" + msg: + - rl_saved + - domain.tld + type: success + - log: + - mailbox + - add + - domain + - active: "1" + aliases: "400" + restart_sogo: "1" + backupmx: "0" + defquota: "3072" + description: some decsription + domain: domain.tld + mailboxes: "10" + maxquota: "10240" + quota: "10240" + relay_all_recipients: "0" + rl_frame: s + rl_value: "10" + tags: ["tag1", "tag2"] + - null + msg: + - domain_added + - domain.tld + type: success + schema: + type: array + items: + type: object + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + description: OK + headers: {} + tags: + - Domains + description: >- + You may create your own domain using this action. It takes a JSON object + containing a domain informations. + operationId: Create domain + requestBody: + content: + application/json: + schema: + example: + active: "1" + aliases: "400" + backupmx: "0" + defquota: "3072" + description: some decsription + domain: domain.tld + mailboxes: "10" + maxquota: "10240" + quota: "10240" + relay_all_recipients: "0" + rl_frame: s + rl_value: "10" + restart_sogo: "10" + tags: ["tag1", "tag2"] + properties: + active: + description: is domain active or not + type: boolean + aliases: + description: limit count of aliases associated with this domain + type: number + backupmx: + description: relay domain or not + type: boolean + defquota: + description: predefined mailbox quota in `add mailbox` form + type: number + description: + description: Description of domain + type: string + domain: + description: Fully qualified domain name + type: string + gal: + description: >- + is domain global address list active or not, it enables + shared contacts accross domain in SOGo webmail + type: boolean + mailboxes: + description: limit count of mailboxes associated with this domain + type: number + maxquota: + description: maximum quota per mailbox + type: number + quota: + description: maximum quota for this domain (for all mailboxes in sum) + type: number + restart_sogo: + description: restart SOGo to activate the domain in SOGo + type: number + relay_all_recipients: + description: >- + if not, them you have to create "dummy" mailbox for each + address to relay + type: boolean + relay_unknown_only: + description: Relay non-existing mailboxes only. Existing mailboxes will be delivered locally. + type: boolean + rl_frame: + enum: + - s + - m + - h + - d + type: string + rl_value: + description: rate limit value + type: number + tags: + description: tags for this Domain + type: array + items: + type: string + type: object + summary: Create domain + /api/v1/add/domain-admin: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - domain_admin + - add + - active: "1" + domains: mailcow.tld + password: "*" + password2: "*" + username: testadmin + msg: + - domain_admin_added + - testadmin + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Domain admin + description: >- + Using this endpoint you can create a new Domain Admin user. This user + has full control over a domain, and can create new mailboxes and + aliases. + operationId: Create Domain Admin user + requestBody: + content: + application/json: + schema: + example: + active: "1" + domains: mailcow.tld + password: supersecurepw + password2: supersecurepw + username: testadmin + properties: + active: + description: 1 for a active user account 0 for a disabled user account + type: number + domains: + description: the domains the user should be a admin of + type: string + password: + description: domain admin user password + type: string + password2: + description: domain admin user password + type: string + username: + description: the username for the admin user + type: string + type: object + summary: Create Domain Admin user + /api/v1/add/sso/domain-admin: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + token: "591F6D-5C3DD2-7455CD-DAF1C1-AA4FCC" + description: OK + headers: { } + tags: + - Single Sign-On + description: >- + Using this endpoint you can issue a token for Domain Admin user. This token can be used for + autologin Domain Admin user by using query_string var sso_token={token}. Token expiration time is 30s + operationId: Issue Domain Admin SSO token + requestBody: + content: + application/json: + schema: + example: + username: testadmin + properties: + username: + description: the username for the admin user + type: object + type: object + summary: Issue Domain Admin SSO token + /api/v1/edit/da-acl: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - type: success + log: + - acl + - edit + - testadmin + - username: + - testadmin + da_acl: + - syncjobs + - quarantine + - login_as + - sogo_access + - app_passwds + - bcc_maps + - pushover + - filters + - ratelimit + - spam_policy + - extend_sender_acl + - unlimited_quota + - protocol_access + - smtp_ip_access + - alias_domains + - domain_desc + msg: + - acl_saved + - testadmin + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Domain admin + description: >- + Using this endpoint you can edit the ACLs of a Domain Admin user. This user + has full control over a domain, and can create new mailboxes and + aliases. + operationId: Edit Domain Admin ACL + requestBody: + content: + application/json: + schema: + example: + items: + - testadmin + attr: + da_acl: + - syncjobs + - quarantine + - login_as + - sogo_access + - app_passwds + - bcc_maps + - pushover + - filters + - ratelimit + - spam_policy + - extend_sender_acl + - unlimited_quota + - protocol_access + - smtp_ip_access + - alias_domains + - domain_desc + properties: + items: + description: contains the domain admin username you want to edit + type: object + attr: + properties: + da_acl: + description: contains the list of acl names that are active for this user + type: object + type: object + summary: Edit Domain Admin ACL + /api/v1/edit/domain-admin: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - type: success + log: + - domain_admin + - edit + - username: testadmin + active: ["0","1"] + username_new: testadmin + domains: ["domain.tld"] + password: "*" + password2: "*" + msg: + - domain_admin_modified + - testadmin + schema: + properties: + type: + enum: + - success + - danger + - error + type: string + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: object + description: OK + headers: {} + tags: + - Domain admin + description: >- + Using this endpoint you can edit a existing Domain Admin user. This user + has full control over a domain, and can create new mailboxes and + aliases. + operationId: Edit Domain Admin user + requestBody: + content: + application/json: + schema: + example: + items: + - testadmin + attr: + active: + - '0' + - '1' + username_new: testadmin + domains: ["domain.tld"] + password: supersecurepassword + password2: supersecurepassword + properties: + attr: + properties: + active: + description: is the domain admin active or not + type: boolean + username_new: + description: the username of the domain admin, change this to change the username + type: string + domains: + description: a list of all domains managed by this domain admin + type: array + items: + type: string + password: + description: the new domain admin user password + type: string + password2: + description: the new domain admin user password for confirmation + type: string + type: object + items: + description: contains the domain admin username you want to edit + type: object + summary: Edit Domain Admin user + /api/v1/add/domain-policy: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - policy + - add + - domain + - domain: domain.tld + object_from: "*@baddomain.tld" + object_list: bl + msg: + - domain_modified + - domain.tld + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Domain antispam policies + description: >- + You may create your own domain policy using this action. It takes a JSON + object containing a domain informations. + operationId: Create domain policy + requestBody: + content: + application/json: + schema: + example: + domain: domain.tld + object_from: "*@baddomain.tld" + object_list: bl + properties: + domain: + description: domain name to which policy is associated to + type: string + object_from: + description: exact address or use wildcard to match whole domain + type: string + object_list: + enum: + - wl + - bl + type: string + type: object + summary: Create domain policy + /api/v1/add/fwdhost: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - fwdhost + - add + - filter_spam: "0" + hostname: hosted.mailcow.de + msg: + - forwarding_host_added + - "5.1.76.202, 2a00:f820:417::202" + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Fordwarding Hosts + description: >- + Add a new Forwarding host to mailcow. You can chose to enable or disable + spam filtering of incoming emails by specifing `filter_spam` 0 = + inactive, 1 = active. + operationId: Add Forward Host + requestBody: + content: + application/json: + schema: + example: + filter_spam: "0" + hostname: hosted.mailcow.de + properties: + filter_spam: + description: "1 to enable spam filter, 0 to disable spam filter" + type: number + hostname: + description: contains the hostname you want to add + type: string + type: object + summary: Add Forward Host + /api/v1/add/mailbox: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - mailbox + - add + - mailbox + - active: "1" + domain: domain.tld + local_part: info + name: Full name + password: "*" + password2: "*" + quota: "3072" + force_pw_update: "1" + tls_enforce_in: "1" + tls_enforce_out: "1" + tags: ["tag1", "tag2"] + - null + msg: + - mailbox_added + - info@domain.tld + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Mailboxes + description: >- + You may create your own mailbox using this action. It takes a JSON + object containing a domain informations. + operationId: Create mailbox + requestBody: + content: + application/json: + schema: + example: + active: "1" + domain: domain.tld + local_part: info + name: Full name + authsource: mailcow + password: atedismonsin + password2: atedismonsin + quota: "3072" + force_pw_update: "1" + tls_enforce_in: "1" + tls_enforce_out: "1" + tags: ["tag1", "tag2"] + properties: + active: + description: is mailbox active or not + type: boolean + domain: + description: domain name + type: string + local_part: + description: left part of email address + type: string + name: + description: Full name of the mailbox user + type: string + authsource: + description: Specifies the authentication source for the mailbox. + type: string + enum: [mailcow, ldap, keycloak, generic-oidc] + default: mailcow + password2: + description: mailbox password for confirmation + type: string + password: + description: mailbox password when using `mailcow` as the authentication source. + type: string + quota: + description: mailbox quota + type: number + force_pw_update: + description: forces the user to update its password on first login + type: boolean + tls_enforce_in: + description: force inbound email tls encryption + type: boolean + tls_enforce_out: + description: force oubound tmail tls encryption + type: boolean + type: object + summary: Create mailbox + + /api/v1/add/oauth2-client: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - oauth2 + - add + - client + - redirect_uri: "https://mailcow.tld" + msg: Added client access + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - oAuth Clients + description: Using this endpoint you can create a oAuth clients. + operationId: Create oAuth Client + requestBody: + content: + application/json: + schema: + example: + redirect_uri: "https://mailcow.tld" + properties: + redirect_uri: + description: the uri where you should be redirected after oAuth + type: string + type: object + summary: Create oAuth Client + /api/v1/add/recipient_map: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - recipient_map + - add + - active: "1" + recipient_map_new: target@mailcow.tld + recipient_map_old: recipient@mailcow.tld + - null + msg: + - recipient_map_entry_saved + - recipient@mailcow.tld + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Address Rewriting + description: >- + Using this endpoint you can create a recipient map to forward all mails + from one email address to another. + operationId: Create Recipient Map + requestBody: + content: + application/json: + schema: + example: + active: "1" + recipient_map_new: target@mailcow.tld + recipient_map_old: recipient@mailcow.tld + properties: + active: + description: 1 for a active user account 0 for a disabled user account + type: number + recipient_map_new: + description: the email address that should receive the forwarded emails + type: string + recipient_map_old: + description: the email address which emails should be forwarded + type: string + type: object + summary: Create Recipient Map + /api/v1/add/relayhost: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - relayhost + - add + - hostname: "mailcow.tld:25" + password: supersecurepassword + username: testuser + msg: + - relayhost_added + - "" + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Routing + description: Using this endpoint you can create Sender-Dependent Transports. + operationId: Create Sender-Dependent Transports + requestBody: + content: + application/json: + schema: + example: + hostname: "mailcow.tld:25" + password: supersecurepassword + username: testuser + properties: + hostname: + description: the hostname of the smtp server with port + type: string + password: + description: the password for the smtp user + type: string + username: + description: the username used to authenticate + type: string + type: object + summary: Create Sender-Dependent Transports + /api/v1/add/resource: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - mailbox + - add + - resource + - active: "1" + description: test + domain: mailcow.tld + kind: location + multiple_bookings: "0" + multiple_bookings_custom: "" + multiple_bookings_select: "0" + - null + msg: + - resource_added + - mailcow.tld + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Resources + description: Using this endpoint you can create Resources. + operationId: Create Resources + requestBody: + content: + application/json: + schema: + example: + active: "1" + description: test + domain: mailcow.tld + kind: location + multiple_bookings: "0" + multiple_bookings_custom: "" + multiple_bookings_select: "0" + properties: + active: + description: 1 for a active transport map 0 for a disabled transport map + type: number + description: + description: a description of the resource + type: string + domain: + description: the domain for which the resource should be + type: string + kind: + description: the kind of recouse + enum: + - location + - group + - thing + type: string + multiple_bookings: + enum: + - "-1" + - "1" + - custom + type: string + multiple_bookings_custom: + description: always empty + type: number + multiple_bookings_select: + enum: + - "-1" + - "1" + - custom + type: string + type: object + summary: Create Resources + /api/v1/add/syncjob: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - mailbox + - add + - syncjob + - active: "1" + automap: "1" + custom_params: "" + delete1: "0" + delete2: "0" + delete2duplicates: "1" + enc1: SSL + exclude: (?i)spam|(?i)junk + host1: imap.server.tld + maxage: "0" + maxbytespersecond: "0" + mins_interval: "20" + password1: supersecret + port1: 993 + skipcrossduplicates: "0" + subfolder2: External + subscribeall: "1" + timeout1: "600" + timeout2: "600" + user1: username + username: mailbox@domain.tld + - null + msg: + - mailbox_modified + - mailbox@domain.tld + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Sync jobs + description: >- + You can create new sync job using this action. It takes a JSON object + containing a domain informations. + operationId: Create sync job + summary: Create sync job + requestBody: + content: + application/json: + schema: + example: + username: lisa@mailcow.tld + host1: mail.mailcow.tld + port1: "143" + user1: demo@mailcow.tld + password1: supersecretpw + enc1: TLS + mins_interval: "20" + subfolder2: "/SyncIntoSubfolder" + maxage: "0" + maxbytespersecond: "0" + timeout1: "600" + timeout2: "600" + exclude: "(?i)spam|(?i)junk" + custom_params: "--dry" + delete2duplicates: "1" + delete1: "1" + delete2: "0" + automap: "1" + skipcrossduplicates: "0" + subscribeall: "0" + active: "1" + properties: + parameters: + description: your local mailcow mailbox + type: string + host1: + description: the smtp server where mails should be synced from + type: string + port1: + description: the smtp port of the target mail server + type: string + user1: + description: the username of the mailbox + type: string + password: + description: the password of the mailbox + type: string + enc1: + description: the encryption method used to connect to the mailserver + type: string + mins_internal: + description: the interval in which messages should be syned + type: number + subfolder2: + description: sync into subfolder on destination (empty = do not use subfolder) + type: string + maxage: + description: only sync messages up to this age in days + type: number + maxbytespersecond: + description: max speed transfer limit for the sync + type: number + timeout1: + description: timeout for connection to remote host + type: number + timeout2: + description: timeout for connection to local host + type: number + exclude: + description: exclude objects (regex) + type: string + custom_params: + description: custom parameters + type: string + delete2duplicates: + description: delete duplicates on destination (--delete2duplicates) + type: boolean + delete1: + description: delete from source when completed (--delete1) + type: boolean + delete2: + description: delete messages on destination that are not on source (--delete2) + type: boolean + automap: + description: try to automap folders ("Sent items", "Sent" => "Sent" etc.) (--automap) + type: boolean + skipcrossduplicates: + description: skip duplicate messages across folders (first come, first serve) (--skipcrossduplicates) + type: boolean + subscribeall: + description: subscribe all folders (--subscribeall) + type: boolean + active: + description: enables or disables the sync job + type: boolean + type: object + /api/v1/add/tls-policy-map: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - tls_policy_maps + - add + - parameters: "" + active: "1" + dest: mailcow.tld + policy: encrypt + - null + msg: + - tls_policy_map_entry_saved + - mailcow.tld + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Outgoing TLS Policy Map Overrides + description: Using this endpoint you can create a TLS policy map override. + operationId: Create TLS Policy Map + requestBody: + content: + application/json: + schema: + example: + parameters: "" + active: "1" + dest: mailcow.tld + policy: encrypt + properties: + parameters: + description: >- + custom parameters you find out more about them + [here](http://www.postfix.org/postconf.5.html#smtp_tls_policy_maps) + type: string + active: + description: 1 for a active user account 0 for a disabled user account + type: number + dest: + description: the target domain or email address + type: string + policy: + description: the policy + enum: + - none + - may + - encrypt + - dane + - "'dane" + - fingerprint + - verify + - secure + type: string + type: object + summary: Create TLS Policy Map + /api/v1/add/transport: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - transport + - add + - active: "1" + destination: example2.org + nexthop: "host:25" + password: supersecurepw + username: testuser + msg: + - relayhost_added + - "" + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Routing + description: Using this endpoint you can create Sender-Dependent Transports. + operationId: Create Transport Maps + requestBody: + content: + application/json: + schema: + example: + active: "1" + destination: example.org + nexthop: "host:25" + password: supersecurepw + username: testuser + properties: + active: + description: 1 for a active transport map 0 for a disabled transport map + type: number + destination: + type: string + nexthop: + type: string + password: + description: the password for the smtp user + type: string + username: + description: the username used to authenticate + type: string + type: object + summary: Create Transport Maps + /api/v1/delete/alias: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - mailbox + - delete + - alias + - id: + - "6" + - "9" + - null + msg: + - alias_removed + - alias@domain.tld + type: success + - log: + - mailbox + - delete + - alias + - id: + - "6" + - "9" + - null + msg: + - alias_removed + - alias2@domain.tld + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Aliases + description: You can delete one or more aliases. + operationId: Delete alias + requestBody: + content: + application/json: + schema: + items: + example: "6" + type: string + type: array + summary: Delete alias + /api/v1/delete/app-passwd: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - app_passwd + - delete + - id: + - "2" + msg: + - app_passwd_removed + - "2" + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - App Passwords + description: Using this endpoint you can delete a single app password. + operationId: Delete App Password + requestBody: + content: + application/json: + schema: + example: + - "1" + properties: + items: + description: contains list of app passwords you want to delete + type: object + type: object + summary: Delete App Password + /api/v1/delete/bcc: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - bcc + - delete + - id: + - "4" + - null + msg: + - bcc_deleted + - "4" + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Address Rewriting + description: >- + Using this endpoint you can delete a BCC map, for this you have to know + its ID. You can get the ID using the GET method. + operationId: Delete BCC Map + requestBody: + content: + application/json: + schema: + example: + - "3" + properties: + items: + description: contains list of bcc maps you want to delete + type: object + type: object + summary: Delete BCC Map + /api/v1/delete/dkim: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - dkim + - delete + - domains: + - mailcow.tld + msg: + - dkim_removed + - mailcow.tld + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - DKIM + description: Using this endpoint a existing DKIM Key can be deleted + operationId: Delete DKIM Key + requestBody: + content: + application/json: + schema: + items: + example: + - mailcow.tld + type: string + type: array + summary: Delete DKIM Key + /api/v1/delete/domain: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - mailbox + - delete + - domain + - domain: + - domain.tld + - domain2.tld + - null + msg: + - domain_removed + - domain.tld + type: success + - log: + - mailbox + - delete + - domain + - domain: + - domain.tld + - domain2.tld + - null + msg: + - domain_removed + - domain2.tld + type: success + schema: + type: array + items: + type: object + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + description: OK + headers: {} + tags: + - Domains + description: You can delete one or more domains. + operationId: Delete domain + requestBody: + content: + application/json: + schema: + type: object + example: + - domain.tld + - domain2.tld + properties: + items: + type: array + items: + type: string + summary: Delete domain + /api/v1/delete/domain-admin: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - domain_admin + - delete + - username: + - testadmin + msg: + - domain_admin_removed + - testadmin + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Domain admin + description: Using this endpoint a existing Domain Admin user can be deleted. + operationId: Delete Domain Admin + requestBody: + content: + application/json: + schema: + example: + - testadmin + properties: + items: + description: contains list of usernames of the users you want to delete + type: object + type: object + summary: Delete Domain Admin + /api/v1/delete/domain-policy: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - policy + - delete + - domain + - prefid: + - "1" + - "2" + msg: + - item_deleted + - "1" + type: success + - log: + - policy + - delete + - domain + - prefid: + - "1" + - "2" + msg: + - item_deleted + - "2" + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Domain antispam policies + description: You can delete one o more domain policies. + operationId: Delete domain policy + requestBody: + content: + application/json: + schema: + example: + - "1" + - "2" + properties: + items: + description: contains list of domain policys you want to delete + type: object + type: object + summary: Delete domain policy + /api/v1/delete/fwdhost: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - fwdhost + - delete + - forwardinghost: + - 5.1.76.202 + - "2a00:f820:417::202" + msg: + - forwarding_host_removed + - 5.1.76.202 + type: success + - log: + - fwdhost + - delete + - forwardinghost: + - 5.1.76.202 + - "2a00:f820:417::202" + msg: + - forwarding_host_removed + - "2a00:f820:417::202" + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Fordwarding Hosts + description: >- + Using this endpoint you can delete a forwarding host, in order to do so + you need to know the IP of the host. + operationId: Delete Forward Host + requestBody: + content: + application/json: + schema: + example: + - 5.1.76.202 + - "2a00:f820:417::202" + properties: + ip: + description: contains the ip of the fowarding host you want to delete + type: string + type: object + summary: Delete Forward Host + /api/v1/delete/mailbox: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - mailbox + - delete + - mailbox + - username: + - info@domain.tld + - sales@domain.tld + - null + msg: + - mailbox_removed + - info@domain.tld + type: success + - log: + - mailbox + - delete + - mailbox + - username: + - info@domain.tld + - sales@domain.tld + - null + msg: + - mailbox_removed + - sales@domain.tld + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Mailboxes + description: You can delete one or more mailboxes. + operationId: Delete mailbox + requestBody: + content: + application/json: + schema: + example: + - info@domain.tld + - sales@domain.tld + properties: + items: + description: contains list of mailboxes you want to delete + type: object + type: object + summary: Delete mailbox + /api/v1/delete/mailq: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + msg: Task completed + type: success + description: OK + headers: {} + tags: + - Queue Manager + description: >- + Using this API you can delete the current mail queue. This will delete + all mails in it. + + This API uses the command: `postsuper -d` + operationId: Delete Queue + requestBody: + content: + application/json: + schema: + example: + action: super_delete + properties: + action: + description: use super_delete to delete the mail queue + type: string + type: object + summary: Delete Queue + /api/v1/delete/oauth2-client: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - oauth2 + - delete + - client + - id: + - "1" + msg: + - items_deleted + - "1" + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - oAuth Clients + description: >- + Using this endpoint you can delete a oAuth client, for this you have to + know its ID. You can get the ID using the GET method. + operationId: Delete oAuth Client + requestBody: + content: + application/json: + schema: + example: + - "3" + properties: + items: + description: contains list of oAuth clients you want to delete + type: object + type: object + summary: Delete oAuth Client + /api/v1/delete/qitem: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - quarantine + - delete + - id: + - "33" + msg: + - item_deleted + - "33" + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Quarantine + description: >- + Using this endpoint you can delete a email from quarantine, for this you + have to know its ID. You can get the ID using the GET method. + operationId: Delete mails in Quarantine + requestBody: + content: + application/json: + schema: + example: + - "33" + properties: + items: + description: contains list of emails you want to delete + type: object + type: object + summary: Delete mails in Quarantine + /api/v1/delete/recipient_map: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - recipient_map + - delete + - id: + - "1" + - null + msg: + - recipient_map_entry_deleted + - "1" + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Address Rewriting + description: >- + Using this endpoint you can delete a recipient map, for this you have to + know its ID. You can get the ID using the GET method. + operationId: Delete Recipient Map + requestBody: + content: + application/json: + schema: + example: + - "1" + properties: + items: + description: contains list of recipient maps you want to delete + type: object + type: object + summary: Delete Recipient Map + /api/v1/delete/relayhost: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - relayhost + - delete + - id: + - "1" + msg: + - relayhost_removed + - "1" + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Routing + description: >- + Using this endpoint you can delete a Sender-Dependent Transport, for + this you have to know its ID. You can get the ID using the GET method. + operationId: Delete Sender-Dependent Transports + requestBody: + content: + application/json: + schema: + example: + - "1" + properties: + items: + description: >- + contains list of Sender-Dependent Transport you want to + delete + type: object + type: object + summary: Delete Sender-Dependent Transports + /api/v1/delete/resource: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - mailbox + - delete + - resource + - name: + - test@mailcow.tld + - null + msg: + - resource_removed + - test@mailcow.tld + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Resources + description: >- + Using this endpoint you can delete a Resources, for this you have to + know its ID. You can get the ID using the GET method. + operationId: Delete Resources + requestBody: + content: + application/json: + schema: + example: + - test@mailcow.tld + properties: + items: + description: contains list of Resources you want to delete + type: object + type: object + summary: Delete Resources + /api/v1/delete/syncjob: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + log: + - entity + - action + - object + msg: + - message + - entity name + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Sync jobs + description: You can delete one or more sync jobs. + operationId: Delete sync job + requestBody: + content: + application/json: + schema: + example: + - "6" + - "9" + properties: + items: + description: contains list of aliases you want to delete + type: object + type: object + summary: Delete sync job + /api/v1/delete/tls-policy-map: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - tls_policy_maps + - delete + - id: + - "1" + - null + msg: + - tls_policy_map_entry_deleted + - "1" + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Outgoing TLS Policy Map Overrides + description: >- + Using this endpoint you can delete a TLS Policy Map, for this you have + to know its ID. You can get the ID using the GET method. + operationId: Delete TLS Policy Map + requestBody: + content: + application/json: + schema: + example: + - "3" + properties: + items: + description: contains list of tls policy maps you want to delete + type: object + type: object + summary: Delete TLS Policy Map + /api/v1/delete/transport: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - transport + - delete + - id: + - "1" + msg: + - relayhost_removed + - "1" + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Routing + description: >- + Using this endpoint you can delete a Transport Maps, for this you have + to know its ID. You can get the ID using the GET method. + operationId: Delete Transport Maps + requestBody: + content: + application/json: + schema: + example: + - "1" + properties: + items: + description: contains list of transport maps you want to delete + type: object + type: object + summary: Delete Transport Maps + "/api/v1/delete/mailbox/tag/{mailbox}": + post: + parameters: + - description: name of mailbox + in: path + name: mailbox + example: info@domain.tld + required: true + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - mailbox + - delete + - tags_mailbox + - tags: + - tag1 + - tag2 + mailbox: info@domain.tld + - null + msg: + - mailbox_modified + - info@domain.tld + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Mailboxes + description: You can delete one or more mailbox tags. + operationId: Delete mailbox tags + requestBody: + content: + application/json: + schema: + example: + - tag1 + - tag2 + properties: + items: + description: contains list of mailboxes you want to delete + type: object + type: object + summary: Delete mailbox tags + "/api/v1/delete/domain/tag/{domain}": + post: + parameters: + - description: name of domain + in: path + name: domain + example: domain.tld + required: true + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - mailbox + - delete + - tags_domain + - tags: + - tag1 + - tag2 + domain: domain.tld + - null + msg: + - domain_modified + - domain.tld + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Domains + description: You can delete one or more domain tags. + operationId: Delete domain tags + requestBody: + content: + application/json: + schema: + example: + - tag1 + - tag2 + properties: + items: + description: contains list of domains you want to delete + type: object + type: object + summary: Delete domain tags + /api/v1/edit/alias: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - mailbox + - edit + - alias + - active: "1" + address: alias@domain.tld + goto: destination@domain.tld + id: + - "6" + private_comment: private comment + public_comment: public comment + - null + msg: + - alias_modified + - alias@domain.tld + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Aliases + description: >- + You can update one or more aliases per request. You can also send just + attributes you want to change + operationId: Update alias + requestBody: + content: + application/json: + schema: + example: + attr: + active: "1" + address: alias@domain.tld + goto: destination@domain.tld + private_comment: private comment + public_comment: public comment + items: ["6"] + properties: + attr: + properties: + active: + description: is alias active or not + type: boolean + address: + description: 'alias address, for catchall use "@domain.tld"' + type: string + goto: + description: "destination address, comma separated" + type: string + goto_ham: + description: learn as ham + type: boolean + goto_null: + description: silently ignore + type: boolean + goto_spam: + description: learn as spam + type: boolean + private_comment: + type: string + public_comment: + type: string + sogo_visible: + description: toggle visibility as selectable sender in SOGo + type: boolean + type: object + items: + description: contains list of aliases you want update + type: object + type: object + summary: Update alias + /api/v1/edit/domain: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + schema: + type: array + items: + type: object + properties: + log: + type: array + description: contains request object + items: {} + msg: + type: array + items: {} + type: + enum: + - success + - danger + - error + type: string + description: OK + headers: {} + tags: + - Domains + description: >- + You can update one or more domains per request. You can also send just + attributes you want to change. + + Example: You can add domain names to items list and in attr object just + include `"active": "0"` to deactivate domains. + operationId: Update domain + requestBody: + content: + application/json: + schema: + example: + attr: + active: "1" + aliases: "400" + backupmx: "1" + defquota: "3072" + description: domain description + gal: "1" + mailboxes: "10" + maxquota: "10240" + quota: "10240" + relay_all_recipients: "0" + relayhost: "2" + tags: ["tag3", "tag4"] + items: domain.tld + properties: + attr: + properties: + active: + description: is domain active or not + type: boolean + aliases: + description: limit count of aliases associated with this domain + type: number + backupmx: + description: relay domain or not + type: boolean + defquota: + description: predefined mailbox quota in `add mailbox` form + type: number + description: + description: Description of domain + type: string + gal: + description: >- + is domain global address list active or not, it enables + shared contacts accross domain in SOGo webmail + type: boolean + mailboxes: + description: limit count of mailboxes associated with this domain + type: number + maxquota: + description: maximum quota per mailbox + type: number + quota: + description: maximum quota for this domain (for all mailboxes in sum) + type: number + relay_all_recipients: + description: >- + if not, them you have to create "dummy" mailbox for each + address to relay + type: boolean + relay_unknown_only: + description: Relay non-existing mailboxes only. Existing mailboxes will be delivered locally. + type: boolean + relayhost: + description: id of relayhost + type: number + rl_frame: + enum: + - s + - m + - h + - d + type: string + rl_value: + description: rate limit value + type: number + tags: + description: tags for this Domain + type: array + items: + type: string + type: object + items: + description: contains list of domain names you want update + type: array + items: + type: string + type: object + summary: Update domain + /api/v1/edit/domain/footer: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - mailbox + - edit + - domain_wide_footer + - domains: + - mailcow.tld + html: "
foo {= foo =}" + plain: "- + You can update the footer of one or more domains per request. + operationId: Update domain wide footer + requestBody: + content: + application/json: + schema: + example: + attr: + html: "
foo {= foo =}" + plain: "foo {= foo =}" + mbox_exclude: + - moo@mailcow.tld + items: mailcow.tld + properties: + attr: + properties: + html: + description: Footer text in HTML format + type: string + plain: + description: Footer text in PLAIN text format + type: string + mbox_exclude: + description: Array of mailboxes to exclude from domain wide footer + type: object + type: object + items: + description: contains a list of domain names where you want to update the footer + type: array + items: + type: string + type: object + summary: Update domain wide footer + /api/v1/edit/fail2ban: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + "*/*": + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Fail2Ban + description: >- + Using this endpoint you can edit the Fail2Ban config and black or + whitelist new ips. + operationId: Edit Fail2Ban + requestBody: + content: + application/json: + schema: + example: + attr: + ban_time: "86400" + ban_time_increment: "1" + blacklist: "10.100.6.5/32,10.100.8.4/32" + max_attempts: "5" + max_ban_time: "86400" + netban_ipv4: "24" + netban_ipv6: "64" + retry_window: "600" + whitelist: mailcow.tld + items: none + properties: + attr: + description: array containing the fail2ban settings + properties: + backlist: + description: the backlisted ips or hostnames separated by comma + type: string + ban_time: + description: the time an ip should be banned + type: number + ban_time_increment: + description: if the time of the ban should increase each time + type: boolean + max_attempts: + description: the maximum numbe of wrong logins before a ip is banned + type: number + max_ban_time: + description: the maximum time an ip should be banned + type: number + netban_ipv4: + description: the networks mask to ban for ipv4 + type: number + netban_ipv6: + description: the networks mask to ban for ipv6 + type: number + retry_window: + description: >- + the maximum time in which a ip as to login with false + credentials to be banned + type: number + whitelist: + description: whitelisted ips or hostnames sepereated by comma + type: string + type: object + items: + description: has to be none + type: object + summary: Edit Fail2Ban + /api/v1/edit/mailbox: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - mailbox + - edit + - mailbox + - active: "1" + force_pw_update: "0" + name: Full name + password: "*" + password2: "*" + quota: "3072" + sender_acl: + - default + - info@domain2.tld + - domain3.tld + - "*" + sogo_access: "1" + username: + - info@domain.tld + tags: ["tag3", "tag4"] + - null + msg: + - mailbox_modified + - info@domain.tld + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Mailboxes + description: >- + You can update one or more mailboxes per request. You can also send just + attributes you want to change + operationId: Update mailbox + requestBody: + content: + application/json: + schema: + example: + attr: + active: "1" + force_pw_update: "0" + name: Full name + authsource: mailcow + password: "" + password2: "" + quota: "3072" + sender_acl: + - default + - info@domain2.tld + - domain3.tld + - "*" + sogo_access: "1" + tags: ["tag3", "tag4"] + items: + - info@domain.tld + properties: + attr: + properties: + active: + description: is mailbox active or not + type: boolean + force_pw_update: + description: force user to change password on next login + type: boolean + name: + description: Full name of the mailbox user + type: string + authsource: + description: Specifies the authentication source for the mailbox. + type: string + enum: [mailcow, ldap, keycloak, generic-oidc] + password2: + description: new mailbox password for confirmation + type: string + password: + description: new mailbox password when using `mailcow` as the authentication source. + type: string + quota: + description: mailbox quota + type: number + sender_acl: + description: list of allowed send from addresses + type: object + sogo_access: + description: is access to SOGo webmail active or not + type: boolean + type: object + items: + description: contains list of mailboxes you want update + type: object + type: object + summary: Update mailbox + /api/v1/edit/mailbox/custom-attribute: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - mailbox + - edit + - mailbox_custom_attribute + - mailboxes: + - moo@mailcow.tld + attribute: + - role + - foo + value: + - cow + - bar + - null + msg: + - mailbox_modified + - moo@mailcow.tld + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Mailboxes + description: >- + You can update custom attributes of one or more mailboxes per request. + operationId: Update mailbox custom attributes + requestBody: + content: + application/json: + schema: + example: + attr: + attribute: + - role + - foo + value: + - cow + - bar + items: + - moo@mailcow.tld + properties: + attr: + properties: + attribute: + description: Array of attribute keys + type: object + value: + description: Array of attribute values + type: object + type: object + items: + description: contains list of mailboxes you want update + type: object + type: object + summary: Update mailbox custom attributes + /api/v1/edit/mailq: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + msg: Task completed + type: success + description: OK + headers: {} + tags: + - Queue Manager + description: >- + Using this API you can flush the current mail queue. This will try to + deliver all mails currently in it. + + This API uses the command: `postqueue -f` + operationId: Flush Queue + requestBody: + content: + application/json: + schema: + example: + action: flush + properties: + action: + description: use flush to flush the mail queue + type: string + type: object + summary: Flush Queue + /api/v1/edit/pushover: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - pushover + - edit + - active: "0" + evaluate_x_prio: "0" + key: 21e8918e1jksdjcpis712 + only_x_prio: "0" + sound: "pushover" + senders: "" + senders_regex: "" + text: "" + title: Mail + token: 9023e2ohcwed27d1idu2 + username: + - info@domain.tld + msg: pushover_settings_edited + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Mailboxes + description: >- + Using this endpoint it is possible to update the pushover settings for + mailboxes + operationId: Update Pushover settings + requestBody: + content: + application/json: + schema: + example: + attr: + active: "0" + evaluate_x_prio: "0" + key: 21e8918e1jksdjcpis712 + only_x_prio: "0" + sound: "pushover" + senders: "" + senders_regex: "" + text: "" + title: Mail + token: 9023e2ohcwed27d1idu2 + items: info@domain.tld + properties: + attr: + properties: + active: + description: Enables pushover 1 disable pushover 0 + type: number + evaluate_x_prio: + description: Send the Push with High priority + type: number + key: + description: Pushover key + type: string + only_x_prio: + description: Only send push for prio mails + type: number + sound: + description: Set notification sound + type: string + senders: + description: Only send push for emails from these senders + type: string + senders_regex: + description: Regex to match senders for which a push will be send + type: string + text: + description: Custom push noficiation text + type: string + title: + description: Push title + type: string + token: + description: Pushover token + type: string + type: object + items: + description: contains list of mailboxes you want to delete + type: object + type: object + summary: Update Pushover settings + /api/v1/edit/quarantine_notification: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + description: OK + headers: {} + tags: + - Mailboxes + description: You can update one or more mailboxes per request. + operationId: Quarantine Notifications + requestBody: + content: + application/json: + schema: + example: + attr: + quarantine_notification: hourly + items: + anyOf: + - mailbox1@domain.tld + - mailbox2@domain.tld + properties: + attr: + properties: + quarantine_notification: + description: recurrence + enum: + - hourly + - daily + - weekly + - never + type: string + type: object + items: + description: >- + contains list of mailboxes you want set qurantine + notifications + type: object + type: object + summary: Quarantine Notifications + /api/v1/edit/syncjob: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + log: + - entity + - action + - object + msg: + - message + - entity name + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Sync jobs + description: >- + You can update one or more sync jobs per request. You can also send just + attributes you want to change. + operationId: Update sync job + requestBody: + content: + application/json: + schema: + example: + attr: + active: "1" + automap: "1" + custom_params: "" + delete1: "0" + delete2: "0" + delete2duplicates: "1" + enc1: SSL + exclude: (?i)spam|(?i)junk + host1: imap.server.tld + maxage: "0" + maxbytespersecond: "0" + mins_interval: "20" + password1: supersecret + port1: "993" + skipcrossduplicates: "0" + subfolder2: External + subscribeall: "1" + timeout1: "600" + timeout2: "600" + user1: username + items: "1" + properties: + attr: + properties: + active: + description: Is sync job active + type: boolean + automap: + description: >- + Try to automap folders ("Sent items", "Sent" => "Sent" + etc.) + type: boolean + custom_params: + description: Custom parameters passed to imapsync command + type: string + delete1: + description: Delete from source when completed + type: boolean + delete2: + description: Delete messages on destination that are not on source + type: boolean + delete2duplicates: + description: Delete duplicates on destination + type: boolean + enc1: + description: Encryption + enum: + - TLS + - SSL + - PLAIN + type: string + exclude: + description: Exclude objects (regex) + type: string + host1: + description: Hostname + type: string + maxage: + description: >- + Maximum age of messages in days that will be polled from + remote (0 = ignore age) + type: number + maxbytespersecond: + description: Max. bytes per second (0 = unlimited) + type: number + mins_interval: + description: Interval (min) + type: number + password1: + description: Password + type: string + port1: + description: Port + type: string + skipcrossduplicates: + description: >- + Skip duplicate messages across folders (first come, + first serve) + type: boolean + subfolder2: + description: >- + Sync into subfolder on destination (empty = do not use + subfolder) + type: string + subscribeall: + description: Subscribe all folders + type: boolean + timeout1: + description: Timeout for connection to remote host + type: number + timeout2: + description: Timeout for connection to local host + type: number + user1: + description: Username + type: string + type: object + items: + description: contains list of aliases you want update + type: object + type: object + summary: Update sync job + /api/v1/edit/user-acl: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - log: + - acl + - edit + - user + - user_acl: + - spam_alias + - tls_policy + - spam_score + - spam_policy + - delimiter_action + - syncjobs + - eas_reset + - quarantine + - sogo_profile_reset + - quarantine_attachments + - quarantine_notification + - app_passwds + - pushover + username: + - info@domain.tld + msg: + - acl_saved + - info@domain.tld + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Mailboxes + description: Using this endpoints its possible to update the ACL's for mailboxes + operationId: Update mailbox ACL + requestBody: + content: + application/json: + schema: + example: + attr: + user_acl: + - spam_alias + - tls_policy + - spam_score + - spam_policy + - delimiter_action + - syncjobs + - eas_reset + - quarantine + - sogo_profile_reset + - quarantine_attachments + - quarantine_notification + - app_passwds + - pushover + items: info@domain.tld + properties: + attr: + properties: + user_acl: + description: contains a list of active user acls + type: object + type: object + items: + description: contains list of mailboxes you want to delete + type: object + type: object + summary: Update mailbox ACL + "/api/v1/get/alias/{id}": + get: + parameters: + - description: id of entry you want to get + example: all + in: path + name: id + required: true + schema: + enum: + - all + - "1" + - "2" + - "5" + - "10" + type: string + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - active: "1" + address: alias@domain.tld + created: "2019-04-04 19:29:49" + domain: domain.tld + goto: destination@domain.tld + id: 6 + in_primary_domain: "" + is_catch_all: 0 + modified: null + private_comment: null + public_comment: null + - active: "1" + address: "@domain.tld" + created: "2019-04-27 13:42:39" + domain: domain.tld + goto: destination@domain.tld + id: 10 + in_primary_domain: "" + is_catch_all: 1 + modified: null + private_comment: null + public_comment: null + description: OK + headers: {} + tags: + - Aliases + description: You can list mailbox aliases existing in system. + operationId: Get aliases + summary: Get aliases + "/api/v1/get/time_limited_aliases/{mailbox}": + get: + parameters: + - description: mailbox you want to get aliasses from + example: domain.tld + in: path + schema: + type: string + name: mailbox + required: true + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - address: alias@domain.tld + goto: destination@domain.tld + validity: 1668251246 + created: "2021-11-12 12:07:26" + modified: null + description: OK + headers: {} + tags: + - Aliases + description: You can list time limited mailbox aliases existing in system. + operationId: Get time limited aliases + summary: Get time limited aliases + "/api/v1/get/app-passwd/all/{mailbox}": + get: + parameters: + - description: mailbox of entry you want to get + example: hello@mailcow.email + in: path + name: mailbox + required: true + schema: + enum: + - hello@mailcow.email + type: string + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - active: "1" + created: "2019-12-21 16:04:55" + domain: mailcow.email + id: 2 + mailbox: hello@mailcow.email + modified: null + name: emclient + description: OK + headers: {} + tags: + - App Passwords + description: >- + Using this endpoint you can get all app passwords from a specific + mailbox. + operationId: Get App Password + summary: Get App Password + "/api/v1/get/bcc/{id}": + get: + parameters: + - description: id of entry you want to get + example: all + in: path + name: id + required: true + schema: + enum: + - all + - "1" + - "2" + - "5" + - "10" + type: string + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - active: "1" + bcc_dest: bcc@awesomecow.tld + created: "2019-10-02 21:44:34" + domain: mailcow.tld + id: 3 + local_dest: "@mailcow.tld" + modified: null + type: sender + description: OK + headers: {} + tags: + - Address Rewriting + description: Using this endpoint you can get all BCC maps. + operationId: Get BCC Map + summary: Get BCC Map + "/api/v1/get/dkim/{domain}": + get: + parameters: + - description: name of domain + in: path + name: domain + required: true + schema: + type: string + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + dkim_selector: dkim + dkim_txt: >- + v=DKIM1;k=rsa;t=s;s=email;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA21tUSjyasQy/hJmVjPnlRGfzx6TPhYj8mXY9DVOzSAE64Gddw/GnE/GcCR6WXNT23u9q4zPnz1IPoNt5kFOps8vg/iNqrcH++494noaZuYyFPPFnebkfryO4EvEyxC/c66qts+gnOUml+M8uv5WObBJld2gG12jLwFM0263J/N6J8LuUsaXOB2uCIfx8Nf4zjuJ6Ieez2uyHNK5dXjDLfKA4mTr+EEK6W6e34M4KN1liWM6r9Oy5S1FlLrD42VpURxxBZtBiEtaJPEKSQuk6GQz8ihu7W20Yr53tyCdaORu8dhxXVUWVf+GjuuMEdAmQCjYkarXdYCrt56Psw703kwIDAQAB + length: "2048" + privkey: "" + pubkey: >- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA21tUSjyasQy/hJmVjPnlRGfzx6TPhYj8mXY9DVOzSAE64Gddw/GnE/GcCR6WXNT23u9q4zPnz1IPoNt5kFOps8vg/iNqrcH++494noaZuYyFPPFnebkfryO4EvEyxC/c66qts+gnOUml+M8uv5WObBJld2gG12jLwFM0263J/N6J8LuUsaXOB2uCIfx8Nf4zjuJ6Ieez2uyHNK5dXjDLfKA4mTr+EEK6W6e34M4KN1liWM6r9Oy5S1FlLrD42VpURxxBZtBiEtaJPEKSQuk6GQz8ihu7W20Yr53tyCdaORu8dhxXVUWVf+GjuuMEdAmQCjYkarXdYCrt56Psw703kwIDAQAB + description: OK + headers: {} + tags: + - DKIM + description: >- + Using this endpoint you can get the DKIM public key for a specific + domain. + operationId: Get DKIM Key + summary: Get DKIM Key + /api/v1/get/domain-admin/all: + get: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - active: "1" + created: "2019-10-02 10:29:41" + selected_domains: + - mailcow.tld + tfa_active: "0" + unselected_domains: + - awesomemailcow.de + - mailcowisgreat.de + username: testadmin + description: OK + headers: {} + tags: + - Domain admin + description: "" + operationId: Get Domain Admins + summary: Get Domain Admins + "/api/v1/get/domain/{id}": + get: + parameters: + - description: id of entry you want to get + example: all + in: path + name: id + required: true + schema: + enum: + - all + - mailcow.tld + type: string + - description: comma seperated list of tags to filter by + example: "tag1,tag2" + in: query + name: tags + required: false + schema: + type: string + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - active: "1" + aliases_in_domain: 0 + aliases_left: 400 + backupmx: "0" + bytes_total: "5076666944" + def_new_mailbox_quota: 3221225472 + def_quota_for_mbox: 3221225472 + description: Some description + domain_name: domain.tld + gal: "0" + max_new_mailbox_quota: 10737418240 + max_num_aliases_for_domain: 400 + max_num_mboxes_for_domain: 10 + max_quota_for_domain: 10737418240 + max_quota_for_mbox: 10737418240 + mboxes_in_domain: 0 + mboxes_left: 10 + msgs_total: "172440" + quota_used_in_domain: "0" + relay_all_recipients: "0" + relayhost: "0" + rl: false + tags: ["tag1", "tag2"] + - active: "1" + aliases_in_domain: 0 + aliases_left: 400 + backupmx: "1" + bytes_total: "5076666944" + def_new_mailbox_quota: 3221225472 + def_quota_for_mbox: 3221225472 + description: domain description + domain_name: domain2.tld + gal: "0" + max_new_mailbox_quota: 10737418240 + max_num_aliases_for_domain: 400 + max_num_mboxes_for_domain: 10 + max_quota_for_domain: 10737418240 + max_quota_for_mbox: 10737418240 + mboxes_in_domain: 0 + mboxes_left: 10 + msgs_total: "172440" + quota_used_in_domain: "0" + relay_all_recipients: "0" + relayhost: "0" + rl: false + tags: ["tag3", "tag4"] + description: OK + headers: {} + tags: + - Domains + description: You can list all domains existing in system. + operationId: Get domains + summary: Get domains + /api/v1/get/fail2ban: + get: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + ban_time: 604800 + ban_time_increment: 1 + blacklist: |- + 45.82.153.37/32 + 92.118.38.52/32 + max_attempts: 1 + max_ban_time: 604800 + netban_ipv4: 32 + netban_ipv6: 128 + perm_bans: + - 45.82.153.37/32 + - 92.118.38.52/32 + retry_window: 7200 + whitelist: 1.1.1.1 + description: OK + headers: {} + tags: + - Fail2Ban + description: Gets the current Fail2Ban configuration. + operationId: Get Fail2Ban Config + summary: Get Fail2Ban Config + /api/v1/get/fwdhost/all: + get: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - host: 5.1.76.202 + keep_spam: "yes" + source: hosted.mailcow.de + - host: "2a00:f820:417::202" + keep_spam: "yes" + source: hosted.mailcow.de + description: OK + headers: {} + tags: + - Fordwarding Hosts + description: You can list all Forwarding Hosts in your mailcow. + operationId: Get Forwarding Hosts + summary: Get Forwarding Hosts + "/api/v1/get/logs/acme/{count}": + get: + parameters: + - description: Number of logs to return + in: path + name: count + required: true + schema: + type: number + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - message: >- + Certificate validation done, neither changed nor due for + renewal, sleeping for another day. + time: "1569927728" + description: OK + headers: {} + tags: + - Logs + description: >- + This Api endpoint lists all ACME logs from issued Lets Enctypts + certificates. + + Tip: You can limit how many logs you want to get by using `/` at + the end of the api url. + operationId: Get ACME logs + summary: Get ACME logs + "/api/v1/get/logs/api/{count}": + get: + parameters: + - description: Number of logs to return + in: path + name: count + required: true + schema: + type: number + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - data: "" + method: GET + remote: 1.1.1.1 + time: 1569939001 + uri: /api/v1/get/logs/api/2 + description: OK + headers: {} + tags: + - Logs + description: >- + This Api endpoint lists all Api logs. + + Tip: You can limit how many logs you want to get by using `/` at + the end of the api url. + operationId: Get Api logs + summary: Get Api logs + "/api/v1/get/logs/autodiscover/{count}": + get: + parameters: + - description: Number of logs to return + in: path + name: count + required: true + schema: + type: number + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - service: activesync + time: 1569684212 + ua: >- + Microsoft Office/16.0 (Windows NT 6.2; MAPICPL + 16.0.11328; Pro) + user: awesome@mailcow.de + description: OK + headers: {} + tags: + - Logs + description: >- + This Api endpoint lists all Autodiscover logs. + + Tip: You can limit how many logs you want to get by using `/` at + the end of the api url. + operationId: Get Autodiscover logs + summary: Get Autodiscover logs + "/api/v1/get/logs/dovecot/{count}": + get: + parameters: + - description: Number of logs to return + in: path + name: count + required: true + schema: + type: number + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - message: >- + managesieve-login: Disconnected (no auth attempts in 0 + secs): user=<>, rip=172.22.1.3, lip=172.22.1.250 + priority: info + program: dovecot + time: "1569938740" + description: OK + headers: {} + tags: + - Logs + description: >- + This Api endpoint lists all Dovecot logs. + + Tip: You can limit how many logs you want to get by using `/` at + the end of the api url. + operationId: Get Dovecot logs + summary: Get Dovecot logs + "/api/v1/get/logs/netfilter/{count}": + get: + parameters: + - description: Number of logs to return + in: path + name: count + required: true + schema: + type: number + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - message: "Whitelist was changed, it has 1 entries" + priority: info + time: 1569754911 + - message: Add host/network 1.1.1.1/32 to blacklist + priority: crit + time: 1569754911 + description: OK + headers: {} + tags: + - Logs + description: >- + This Api endpoint lists all Netfilter logs. + + Tip: You can limit how many logs you want to get by using `/` at + the end of the api url. + operationId: Get Netfilter logs + summary: Get Netfilter logs + "/api/v1/get/logs/postfix/{count}": + get: + parameters: + - description: Number of logs to return + in: path + name: count + required: true + schema: + type: number + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - message: "EF1711500458: removed" + priority: info + program: postfix/qmgr + time: "1569937433" + description: OK + headers: {} + tags: + - Logs + description: >- + This Api endpoint lists all Postfix logs. + + Tip: You can limit how many logs you want to get by using `/` at + the end of the api url. + operationId: Get Postfix logs + summary: Get Postfix logs + "/api/v1/get/logs/ratelimited/{count}": + get: + parameters: + - description: Number of logs to return + in: path + name: count + required: true + schema: + type: number + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - from: awesome@mailcow.email + header_from: '"Awesome" ' + header_subject: Mailcow is amazing + ip: 172.22.1.248 + message_id: 6a-5d892500-7-240abd80@90879116 + qid: E3CF91500458 + rcpt: hello@mailcow.email + rl_hash: RLsdz3tuabozgd4oacbdh8kc78 + rl_info: mailcow(RLsdz3tuabozgd4oacbdh8kc78) + rl_name: mailcow + time: 1569269003 + user: awesome@mailcow.email + description: OK + headers: {} + tags: + - Logs + description: >- + This Api endpoint lists all Ratelimit logs. + + Tip: You can limit how many logs you want to get by using `/` at + the end of the api url. + operationId: Get Ratelimit logs + summary: Get Ratelimit logs + "/api/v1/get/logs/rspamd-history/{count}": + get: + parameters: + - description: Number of logs to return + in: path + name: count + required: true + schema: + type: number + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + description: OK + headers: {} + tags: + - Logs + description: >- + This Api endpoint lists all Rspamd logs. + + Tip: You can limit how many logs you want to get by using `/` at + the end of the api url. + operationId: Get Rspamd logs + summary: Get Rspamd logs + "/api/v1/get/logs/sogo/{count}": + get: + parameters: + - description: Number of logs to return + in: path + name: count + required: true + schema: + type: number + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - message: >- + [109]: + mailcowdockerized_watchdog-mailcow_1.mailcowdockerized_mailcow-network + "GET /SOGo.index/ HTTP/1.1" 200 2531/0 0.005 - - 0 + priority: notice + program: sogod + time: "1569938874" + description: OK + headers: {} + tags: + - Logs + description: >- + This Api endpoint lists all SOGo logs. + + Tip: You can limit how many logs you want to get by using `/` at + the end of the api url. + operationId: Get SOGo logs + summary: Get SOGo logs + "/api/v1/get/logs/watchdog/{count}": + get: + parameters: + - description: Number of logs to return + in: path + name: count + required: true + schema: + type: number + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - hpdiff: "0" + hpnow: "1" + hptotal: "1" + lvl: "100" + service: Fail2ban + time: "1569938958" + - hpdiff: "0" + hpnow: "5" + hptotal: "5" + lvl: "100" + service: Rspamd + time: "1569938956" + description: OK + headers: {} + tags: + - Logs + description: >- + This Api endpoint lists all Watchdog logs. + + Tip: You can limit how many logs you want to get by using `/` at + the end of the api url. + operationId: Get Watchdog logs + summary: Get Watchdog logs + "/api/v1/get/mailbox/{id}": + get: + parameters: + - description: id of entry you want to get + example: all + in: path + name: id + required: true + schema: + enum: + - all + - user@domain.tld + type: string + - description: comma seperated list of tags to filter by + example: "tag1,tag2" + in: query + name: tags + required: false + schema: + type: string + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - active: "1" + attributes: + force_pw_update: "0" + mailbox_format: "maildir:" + quarantine_notification: never + sogo_access: "1" + tls_enforce_in: "0" + tls_enforce_out: "0" + domain: doman3.tld + is_relayed: 0 + local_part: info + max_new_quota: 10737418240 + messages: 0 + name: Full name + percent_class: success + percent_in_use: 0 + quota: 3221225472 + quota_used: 0 + rl: false + spam_aliases: 0 + username: info@doman3.tld + tags: ["tag1", "tag2"] + description: OK + headers: {} + tags: + - Mailboxes + description: You can list all mailboxes existing in system. + operationId: Get mailboxes + summary: Get mailboxes + /api/v1/get/mailq/all: + get: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - arrival_time: 1570091234 + message_size: 1848 + queue_id: B98C6260CA1 + queue_name: incoming + recipients: + - recipient@awesomecow.tld + sender: sender@mailcow.tld + description: OK + headers: {} + tags: + - Queue Manager + description: Get the current mail queue and everything it contains. + operationId: Get Queue + summary: Get Queue + "/api/v1/get/oauth2-client/{id}": + get: + parameters: + - description: id of entry you want to get + example: all + in: path + name: id + required: true + schema: + enum: + - all + - "1" + - "2" + - "5" + - "10" + type: string + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - client_id: 17c76aaa88c0 + client_secret: 73fc668a88147e32a31ff80c + grant_types: null + id: 1 + redirect_uri: "https://mailcow.tld" + scope: profile + user_id: null + description: OK + headers: {} + tags: + - oAuth Clients + description: Using this endpoint you can get all oAuth clients. + operationId: Get oAuth Clients + summary: Get oAuth Clients + "/api/v1/get/policy_bl_domain/{domain}": + get: + parameters: + - description: name of domain + in: path + name: domain + required: true + schema: + type: string + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - object: domain.tld + prefid: 2 + value: "*@baddomain.tld" + description: OK + headers: {} + tags: + - Domain antispam policies + description: You can list all blacklist policies per domain. + operationId: List blacklist domain policy + summary: List blacklist domain policy + "/api/v1/get/policy_wl_domain/{domain}": + get: + parameters: + - description: name of domain + in: path + name: domain + required: true + schema: + type: string + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - object: domain.tld + prefid: 1 + value: "*@gooddomain.tld" + description: OK + headers: {} + tags: + - Domain antispam policies + description: You can list all whitelist policies per domain. + operationId: List whitelist domain policy + summary: List whitelist domain policy + /api/v1/get/quarantine/all: + get: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + created: 1572688831 + id: 33 + notified: 1 + qid: 8224615004C1 + rcpt: admin@domain.tld + score: 15.48 + sender: bounces@send.domain.tld + subject: mailcow is awesome + virus_flag: 0 + description: OK + headers: {} + tags: + - Quarantine + description: Get all mails that are currently in Quarantine. + operationId: Get mails in Quarantine + summary: Get mails in Quarantine + "/api/v1/get/recipient_map/{id}": + get: + parameters: + - description: id of entry you want to get + example: all + in: path + name: id + required: true + schema: + enum: + - all + - "1" + - "2" + - "5" + - "10" + type: string + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - active: "1" + created: "2019-10-02 22:06:29" + id: 3 + modified: null + recipient_map_new: target@mailcow.tld + recipient_map_old: recipient@mailcow.tld + description: OK + headers: {} + tags: + - Address Rewriting + description: Using this endpoint you can get all recipient maps. + operationId: Get Recipient Map + summary: Get Recipient Map + "/api/v1/get/relayhost/{id}": + get: + parameters: + - description: id of entry you want to get + example: all + in: path + name: id + required: true + schema: + enum: + - all + - "1" + - "2" + - "5" + - "10" + type: string + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - active: "1" + hostname: "mailcow.tld:25" + id: 1 + password: supersecurepassword + password_short: tes... + used_by_domains: "" + username: testuser + description: OK + headers: {} + tags: + - Routing + description: Using this endpoint you can get all Sender-Dependent Transports. + operationId: Get Sender-Dependent Transports + summary: Get Sender-Dependent Transports + /api/v1/get/resource/all: + get: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - active: "1" + description: test + domain: mailcow.tld + kind: location + local_part: test + multiple_bookings: 0 + name: test@mailcow.tld + description: OK + headers: {} + tags: + - Resources + description: Using this endpoint you can get all Resources. + operationId: Get Resources + summary: Get Resources + "/api/v1/get/rl-mbox/{mailbox}": + get: + parameters: + - description: name of mailbox or all + in: path + name: mailbox + required: true + schema: + type: string + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - frame: s + mailbox: leon@mailcow.tld + value: "5" + - frame: s + mailbox: lisa@mailcow.tld + value: "3" + description: OK + headers: {} + tags: + - Ratelimits + description: >- + Using this endpoint you can get the ratelimits for a certain mailbox. + You can use all for all mailboxes. + operationId: Get mailbox ratelimits + summary: Get mailbox ratelimits + "/api/v1/get/rl-domain/{domain}": + get: + parameters: + - description: name of domain or all + in: path + name: domain + required: true + schema: + type: string + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - frame: s + domain: domain.tld + value: "5" + - frame: s + mailbox: domain2.tld + value: "3" + description: OK + headers: {} + tags: + - Ratelimits + description: >- + Using this endpoint you can get the ratelimits for a certain domains. + You can use all for all domain. + operationId: Get domain ratelimits + summary: Get domain ratelimits + /api/v1/edit/rl-mbox/: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - type: success + log: + - ratelimit + - edit + - mailbox + - object: + - info@domain.tld + rl_value: "10" + rl_frame: h + msg: + - rl_saved + - info@domain.tld + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Ratelimits + description: >- + Using this endpoint you can edit the ratelimits for a certain mailbox. + operationId: Edit mailbox ratelimits + requestBody: + content: + application/json: + schema: + example: + attr: + rl_value: "10" + rl_frame: "h" + items: + - info@domain.tld + properties: + attr: + properties: + rl_frame: + description: contains the frame for the ratelimit h,s,m + type: string + rl_value: + description: contains the rate for the ratelimit 10,20,50,1 + type: number + type: object + items: + description: contains list of mailboxes you want to edit the ratelimit of + type: object + type: object + summary: Edit mailbox ratelimits + /api/v1/edit/rl-domain/: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - type: success + - log: + - ratelimit + - edit + - domain + - object: + - domain.tld + rl_value: "50" + rl_frame: "h" + msg: + - rl_saved + - domain.tld + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Ratelimits + description: >- + Using this endpoint you can edit the ratelimits for a certain domains. + operationId: Edit domain ratelimits + requestBody: + content: + application/json: + schema: + example: + attr: + rl_value: "10" + rl_frame: "h" + items: + - domain.tld + properties: + attr: + properties: + rl_frame: + description: contains the frame for the ratelimit h,s,m + type: string + rl_value: + description: contains the rate for the ratelimit 10,20,50,1 + type: number + type: object + items: + description: contains list of domains you want to edit the ratelimit of + type: object + type: object + summary: Edit domain ratelimits + /api/v1/get/status/containers: + get: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + acme-mailcow: + container: acme-mailcow + image: "mailcow/acme:1.63" + started_at: "2019-12-22T21:00:08.270660275Z" + state: running + type: info + clamd-mailcow: + container: clamd-mailcow + image: "mailcow/clamd:1.35" + started_at: "2019-12-22T21:00:01.622856172Z" + state: running + type: info + dockerapi-mailcow: + container: dockerapi-mailcow + image: "mailcow/dockerapi:1.36" + started_at: "2019-12-22T20:59:59.984797808Z" + state: running + type: info + dovecot-mailcow: + container: dovecot-mailcow + image: "mailcow/dovecot:1.104" + started_at: "2019-12-22T21:00:08.988680259Z" + state: running + type: info + ipv6nat-mailcow: + container: ipv6nat-mailcow + image: robbertkl/ipv6nat + started_at: "2019-12-22T21:06:37.273225445Z" + state: running + type: info + memcached-mailcow: + container: memcached-mailcow + image: "memcached:alpine" + started_at: "2019-12-22T20:59:58.0907785Z" + state: running + type: info + mysql-mailcow: + container: mysql-mailcow + image: "mariadb:10.3" + started_at: "2019-12-22T21:00:02.201937528Z" + state: running + type: info + netfilter-mailcow: + container: netfilter-mailcow + image: "mailcow/netfilter:1.31" + started_at: "2019-12-22T21:00:09.851559297Z" + state: running + type: info + nginx-mailcow: + container: nginx-mailcow + image: "nginx:mainline-alpine" + started_at: "2019-12-22T21:00:12.9843038Z" + state: running + type: info + olefy-mailcow: + container: olefy-mailcow + image: "mailcow/olefy:1.2" + started_at: "2019-12-22T20:59:59.676259274Z" + state: running + type: info + php-fpm-mailcow: + container: php-fpm-mailcow + image: "mailcow/phpfpm:1.55" + started_at: "2019-12-22T21:00:00.955808957Z" + state: running + type: info + postfix-mailcow: + container: postfix-mailcow + image: "mailcow/postfix:1.44" + started_at: "2019-12-22T21:00:07.186717617Z" + state: running + type: info + redis-mailcow: + container: redis-mailcow + image: "redis:5-alpine" + started_at: "2019-12-22T20:59:56.827166834Z" + state: running + type: info + rspamd-mailcow: + container: rspamd-mailcow + image: "mailcow/rspamd:1.56" + started_at: "2019-12-22T21:00:12.456075355Z" + state: running + type: info + sogo-mailcow: + container: sogo-mailcow + image: "mailcow/sogo:1.65" + started_at: "2019-12-22T20:59:58.382274592Z" + state: running + type: info + unbound-mailcow: + container: unbound-mailcow + image: "mailcow/unbound:1.10" + started_at: "2019-12-22T20:59:58.760595825Z" + state: running + type: info + watchdog-mailcow: + container: watchdog-mailcow + image: "mailcow/watchdog:1.65" + started_at: "2019-12-22T20:59:56.028660382Z" + state: running + type: info + description: OK + headers: {} + tags: + - Status + description: >- + Using this endpoint you can get the status of all containers and when + hey where started and a few other details. + operationId: Get container status + summary: Get container status + /api/v1/get/status/vmail: + get: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + disk: /dev/mapper/mail--vg-root + total: 41G + type: info + used: 11G + used_percent: 28% + description: OK + headers: {} + tags: + - Status + description: >- + Using this endpoint you can get the status of the vmail and the amount + of used storage. + operationId: Get vmail status + summary: Get vmail status + /api/v1/get/status/version: + get: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + version: "2022-04" + description: OK + headers: {} + tags: + - Status + description: >- + Using this endpoint you can get the current running release of this + instance. + operationId: Get version status + summary: Get version status + /api/v1/get/syncjobs/all/no_log: + get: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - active: "1" + authmd51: 0 + authmech1: PLAIN + automap: 1 + created: "2019-05-22 11:37:25" + custom_params: "" + delete1: 0 + delete2: 0 + delete2duplicates: 1 + domain2: "" + enc1: TLS + exclude: (?i)spam|(?i)junk + host1: imap.server.tld + id: 1 + is_running: 0 + last_run: "2019-05-22 11:40:02" + log: "" + maxage: 0 + maxbytespersecond: "0" + mins_interval: "20" + modified: "2019-05-22 11:40:02" + port1: 993 + regextrans2: "" + skipcrossduplicates: 0 + subfolder2: External + subscribeall: 1 + timeout1: 600 + timeout2: 600 + user1: username + user2: mailbox@domain.tld + description: OK + headers: {} + tags: + - Sync jobs + description: You can list all syn jobs existing in system. + operationId: Get sync jobs + summary: Get sync jobs + "/api/v1/get/tls-policy-map/{id}": + get: + parameters: + - description: id of entry you want to get + example: all + in: path + name: id + required: true + schema: + enum: + - all + - "1" + - "2" + - "5" + - "10" + type: string + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - parameters: "" + active: "1" + created: "2019-10-03 08:42:12" + dest: mailcow.tld + id: 1 + modified: null + policy: encrypt + description: OK + headers: {} + tags: + - Outgoing TLS Policy Map Overrides + description: Using this endpoint you can get all TLS policy map override maps. + operationId: Get TLS Policy Map + summary: Get TLS Policy Map + "/api/v1/get/transport/{id}": + get: + parameters: + - description: id of entry you want to get + example: all + in: path + name: id + required: true + schema: + enum: + - all + - "1" + - "2" + - "5" + - "10" + type: string + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - active: "1" + destination: example.org + id: 1 + lookup_mx: "0" + nexthop: "host:25" + password: supersecurepw + password_short: sup... + username: testuser + description: OK + headers: {} + tags: + - Routing + description: Using this endpoint you can get all Transport Maps. + operationId: Get Transport Maps + summary: Get Transport Maps + /api/v1/edit/spam-score/: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - type: success + log: + - mailbox + - edit + - spam_score + - username: + - info@domain.tld + spam_score: "8,15" + msg: + - mailbox_modified + - info@domain.tld + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Mailboxes + description: >- + Using this endpoint you can edit the spam filter score for a certain mailbox. + operationId: Edit mailbox spam filter score + requestBody: + content: + application/json: + schema: + example: + - items: + - info@domain.tld + attr: + spam_score: "8,15" + summary: Edit mailbox spam filter score + "/api/v1/get/mailbox/all/{domain}": + get: + parameters: + - description: name of domain + in: path + name: domain + required: true + schema: + type: string + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - active: "1" + attributes: + force_pw_update: "0" + mailbox_format: "maildir:" + quarantine_notification: never + sogo_access: "1" + tls_enforce_in: "0" + tls_enforce_out: "0" + custom_attributes: {} + domain: domain3.tld + is_relayed: 0 + local_part: info + max_new_quota: 10737418240 + messages: 0 + name: Full name + percent_class: success + percent_in_use: 0 + quota: 3221225472 + quota_used: 0 + rl: false + spam_aliases: 0 + username: info@domain3.tld + tags: ["tag1", "tag2"] + description: OK + headers: {} + tags: + - Mailboxes + description: You can list all mailboxes existing in system for a specific domain. + operationId: Get mailboxes of a domain + summary: Get mailboxes of a domain + /api/v1/edit/cors: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - type: "success" + log: ["cors", "edit", {"allowed_origins": ["*", "mail.mailcow.tld"], "allowed_methods": ["POST", "GET", "DELETE", "PUT"]}] + msg: "cors_headers_edited" + description: OK + headers: { } + tags: + - Cross-Origin Resource Sharing (CORS) + description: >- + This endpoint allows you to manage Cross-Origin Resource Sharing (CORS) settings for the API. + CORS is a security feature implemented by web browsers to prevent unauthorized cross-origin requests. + By editing the CORS settings, you can specify which domains and which methods are permitted to access the API resources from outside the mailcow domain. + operationId: Edit Cross-Origin Resource Sharing (CORS) settings + requestBody: + content: + application/json: + schema: + example: + attr: + allowed_origins: ["*", "mail.mailcow.tld"] + allowed_methods: ["POST", "GET", "DELETE", "PUT"] + properties: + attr: + type: object + properties: + allowed_origins: + type: array + items: + type: string + allowed_methods: + type: array + items: + type: string + summary: Edit Cross-Origin Resource Sharing (CORS) settings + "/api/v1/get/spam-score/{mailbox}": + get: + parameters: + - description: name of mailbox or empty for current user - admin user will retrieve the global spam filter score + in: path + name: mailbox + required: true + schema: + type: string + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + spam_score: "8,15" + description: OK + headers: {} + tags: + - Mailboxes + description: >- + Using this endpoint you can get the global spam filter score or the spam filter score of a certain mailbox. + operationId: Get mailbox or global spam filter score + summary: Get mailbox or global spam filter score + /api/v1/edit/identity-provider: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - type: "success" + log: + - "identity_provider" + - "edit" + - authsource: "keycloak" + server_url: "https://auth.mailcow.tld" + realm: "mailcow" + client_id: "mailcow_client" + client_secret: "*" + redirect_url: "https://mail.mailcow.tld" + version: "26.1.3" + default_template: "Default" + mappers: + - "small_mbox" + - "medium_mbox" + templates: + - "small" + - "medium" + ignore_ssl_error: true + mailpassword_flow: true + periodic_sync: true + import_users: true + sync_interval: 30 + msg: + - "object_modified" + - "" + description: OK + headers: { } + tags: + - Identity Provider + description: >- + Configure an external Identity Provider to use as user authentication + operationId: Edit external Identity Provider settings + requestBody: + content: + application/json: + schema: + properties: + items: + type: array + default: ["identity-provider"] + attr: + type: object + properties: + authsource: + description: Specifies the type of the Identity Provider + type: string + enum: [ldap, keycloak, generic-oidc] + server_url: + description: The base URL of your Keycloak server. Required if `authsource` is keycloak. + type: string + realm: + description: The Keycloak realm where the mailcow client is configured. Required if `authsource` is keycloak. + type: string + client_id: + description: The Client ID assigned to mailcow Client in OIDC Provider. Required if `authsource` is keycloak or generic-oidc. + type: string + client_secret: + description: The Client Secret assigned to mailcow Client in OIDC Provider. Required if `authsource` is keycloak or generic-oidc. + type: string + redirect_url: + description: The redirect URL that OIDC Provider will use after authentication. Required if `authsource` is keycloak or generic-oidc. + type: string + version: + description: Specifies the Keycloak version. Required if `authsource` is keycloak. + type: string + default_template: + description: (Optional) If no matching Attribute Mapping exists for a User, the default template will be used for creating the mailbox, but not for updating the mailbox. + type: string + mappers: + description: (Optional) Attribute values used to match a mailbox template. Each element corresponds to the respective index in the templates array (i.e., the first element matches the first element of templates, the second matches the second, and so on). + type: array + templates: + description: (Optional) Defines the mailbox templates to be assigned. Each element corresponds to the respective index in the `mappers` array. + type: array + ignore_ssl_error: + description: If enabled, SSL certificate validation is bypassed + type: boolean + default: false + mailpassword_flow: + description: If enabled, mailcow will attempt to validate user credentials using the Keycloak Admin REST API instead of relying solely on the Authorization Code Flow. + type: boolean + default: false + periodic_sync: + description: If enabled, mailcow periodically performs a full sync of all users from Keycloak or LDAP. + type: boolean + default: false + import_users: + description: If enabled, new users are automatically imported from Keycloak or LDAP into mailcow. + type: boolean + default: false + sync_interval: + description: Defines the time interval (in minutes) for periodic synchronization and user imports. + type: number + default: 15 + host: + description: The address of your LDAP server. You can provide a single hostname or a comma-separated list of hosts for fallback in case the primary server is unreachable. Required if `authsource` is ldap. + type: string + port: + description: The port used to connect to the LDAP server. Required if `authsource` is ldap. + type: string + use_ssl: + description: enable LDAPS connection. If Port is set to 389 it will be overriden to 636. + type: boolean + default: false + use_tls: + description: enable TLS connection. TLS is recommended over SSL. SSL Ports cannot be used. + type: boolean + default: false + basedn: + description: The Distinguished Name (DN) from which searches will be performed. Required if `authsource` is ldap. + type: string + username_field: + description: The LDAP attribute used to identify users during authentication. Required if `authsource` is ldap. + type: string + default: mail + filter: + description: An optional LDAP search filter to refine which users can authenticate. + type: string + attribute_field: + description: Specifies an LDAP attribute that holds a specific value which can be mapped to a mailbox template using the Attribute Mapping section. Required if `authsource` is ldap. + type: string + binddn: + description: The Distinguished Name (DN) of the LDAP user that will be used to authenticate and perform LDAP searches. This account should have sufficient permissions to read the required attributes. Required if `authsource` is ldap. + type: string + bindpass: + description: The password for the Bind DN user. It is required for authentication when connecting to the LDAP server. Required if `authsource` is ldap. + type: string + authorize_url: + description: The OIDC provider's authorization server URL. Required if `authsource` is generic-oidc. + type: string + token_url: + description: The OIDC provider's token server URL. Required if `authsource` is generic-oidc. + type: string + userinfo_url: + description: The OIDC provider's user info server URL. Required if `authsource` is generic-oidc. + type: string + client_scopes: + description: Specifies the OIDC scopes requested during authentication. + type: string + default: "openid profile email mailcow_template" + examples: + keycloak: + value: + items: + - "identity-provider" + attr: + authsource: "keycloak" + server_url: "https://auth.mailcow.tld" + realm: "mailcow" + client_id: "mailcow_client" + client_secret: "Xy7GdPqvJ9m3R8sT2LkVZ5W1oNbCaYQf" + redirect_url: "https://mail.mailcow.tld" + version: "26.1.3" + default_template: "Default" + mappers: ["small_mbox", "medium_mbox"] + templates: ["small", "medium"] + ignore_ssl_error: true + mailpassword_flow: true + periodic_sync: true + import_users: true + sync_interval: 30 + ldap: + value: + items: + - "identity-provider" + attr: + authsource: "ldap" + host: "127.0.0.1" + port: "389" + use_ssl: false + use_tls: false + ignore_ssl_error: false + basedn: "DC=mailcow,DC=local" + username_field: "mail" + filter: "(memberOf:1.2.840.113556.1.4.1941:=DC=mailcow,DC=local)" + attribute_field: "othermailbox" + binddn: "CN=LDAP Read Only,CN=Users,DC=mailcow,DC=local" + bindpass: "moohoo" + default_template: "Default" + mappers: ["small_mbox", "medium_mbox"] + templates: ["small", "medium"] + periodic_sync: true + import_users: true + sync_interval: 30 + generic-oidc: + value: + items: + - "identity-provider" + attr: + authsource: "generic-oidc" + authorize_url: "https://auth.mailcow.tld/application/o/authorize/" + token_url: "https://auth.mailcow.tld/application/o/token/" + userinfo_url: "https://auth.mailcow.tld/application/o/userinfo/" + client_id: "mailcow_client" + client_secret: "Xy7GdPqvJ9m3R8sT2LkVZ5W1oNbCaYQf" + redirect_url: "https://mail.mailcow.tld" + client_scopes: "openid profile email mailcow_template" + default_template: "Default" + mappers: ["small_mbox", "medium_mbox"] + templates: ["small", "medium"] + ignore_ssl_error: true + summary: Edit external Identity Provider + +tags: + - name: Domains + description: You can create antispam whitelist and blacklist policies + - name: Domain antispam policies + description: You can edit the Domain Antispam policies + - name: Mailboxes + description: You can manage mailboxes + - name: Aliases + description: You can manage aliases + - name: Sync jobs + description: Using Syncjobs you can sync your mails with other email servers + - name: Fordwarding Hosts + description: Forwarding Hosts enable you to send mail using a relay + - name: Logs + description: Get all mailcow system logs + - name: Queue Manager + description: Manage the postfix mail queue + - name: Quarantine + description: Check what emails went to quarantine + - name: Fail2Ban + description: Manage the Netfilter fail2ban options + - name: DKIM + description: Manage DKIM keys + - name: Domain admin + description: Create or udpdate domain admin users + - name: Single Sign-On + description: Issue tokens for users + - name: Address Rewriting + description: Create BCC maps or recipient maps + - name: Outgoing TLS Policy Map Overrides + description: Force global TLS policys + - name: oAuth Clients + description: Use mailcow as a oAuth server + - name: Routing + description: Define your own email routes + - name: Resources + description: Manage ressources + - name: App Passwords + description: Create mailbox app passwords + - name: Status + description: Get the status of your cow + - name: Ratelimits + description: Edit domain ratelimits + - name: Cross-Origin Resource Sharing (CORS) + description: Manage Cross-Origin Resource Sharing (CORS) settings + - name: Identity Provider + description: Manage external Identity Provider settings diff --git a/mailcow/data/web/api/swagger-initializer.js b/mailcow/data/web/api/swagger-initializer.js new file mode 100644 index 0000000..13187cb --- /dev/null +++ b/mailcow/data/web/api/swagger-initializer.js @@ -0,0 +1,18 @@ +window.onload = function() { + // Begin Swagger UI call region + window.ui = SwaggerUIBundle({ + urls: [{url: "/api/openapi.yaml", name: "mailcow API"}], + dom_id: '#swagger-ui', + deepLinking: true, + presets: [ + SwaggerUIBundle.presets.apis, + SwaggerUIStandalonePreset + ], + plugins: [ + SwaggerUIBundle.plugins.DownloadUrl + ], + layout: "StandaloneLayout" + }); + // End Swagger UI call region + +}; diff --git a/mailcow/data/web/api/swagger-ui-bundle.js b/mailcow/data/web/api/swagger-ui-bundle.js new file mode 100644 index 0000000..b628d7f --- /dev/null +++ b/mailcow/data/web/api/swagger-ui-bundle.js @@ -0,0 +1,3 @@ +/*! For license information please see swagger-ui-bundle.js.LICENSE.txt */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.SwaggerUIBundle=t():e.SwaggerUIBundle=t()}(this,(()=>(()=>{var e={17967:(e,t)=>{"use strict";t.N=void 0;var n=/^([^\w]*)(javascript|data|vbscript)/im,r=/&#(\w+)(^\w|;)?/g,o=/&(newline|tab);/gi,s=/[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/gim,i=/^.+(:|:)/gim,a=[".","/"];t.N=function(e){var t,l=(t=e||"",t.replace(r,(function(e,t){return String.fromCharCode(t)}))).replace(o,"").replace(s,"").trim();if(!l)return"about:blank";if(function(e){return a.indexOf(e[0])>-1}(l))return l;var c=l.match(i);if(!c)return l;var u=c[0];return n.test(u)?"about:blank":l}},53795:(e,t,n)=>{"use strict";n.d(t,{Z:()=>P});var r=n(23101),o=n.n(r),s=n(61125),i=n.n(s),a=n(11882),l=n.n(a),c=n(97606),u=n.n(c),p=n(67294),h=n(43393);function f(e){return f="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},f(e)}function d(e,t){for(var n=0;n1&&void 0!==arguments[1]?arguments[1]:{},n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=function(e,t){return function(n){if("string"==typeof n)return(0,h.is)(t[n],e[n]);if(Array.isArray(n))return(0,h.is)(x(t,n),x(e,n));throw new TypeError("Invalid key: expected Array or string: "+n)}}(t,n),o=e||Object.keys(function(e){for(var t=1;t1&&void 0!==arguments[1]?arguments[1]:{};return!S(this.updateOnProps,this.props,e,"updateOnProps")||!S(this.updateOnStates,this.state,t,"updateOnStates")}}],r&&d(n.prototype,r),o&&d(n,o),t}(p.Component);var j=n(23930),O=n.n(j),k=n(45697),A=n.n(k);const C=e=>{const t=e.replace(/~1/g,"/").replace(/~0/g,"~");try{return decodeURIComponent(t)}catch{return t}};class P extends _{constructor(){super(...arguments),i()(this,"getModelName",(e=>-1!==l()(e).call(e,"#/definitions/")?C(e.replace(/^.*#\/definitions\//,"")):-1!==l()(e).call(e,"#/components/schemas/")?C(e.replace(/^.*#\/components\/schemas\//,"")):void 0)),i()(this,"getRefSchema",(e=>{let{specSelectors:t}=this.props;return t.findDefinition(e)}))}render(){let{getComponent:e,getConfigs:t,specSelectors:r,schema:s,required:i,name:a,isRef:l,specPath:c,displayName:u,includeReadOnly:h,includeWriteOnly:f}=this.props;const d=e("ObjectModel"),m=e("ArrayModel"),g=e("PrimitiveModel");let y="object",v=s&&s.get("$$ref");if(!a&&v&&(a=this.getModelName(v)),!s&&v&&(s=this.getRefSchema(a)),!s)return p.createElement("span",{className:"model model-title"},p.createElement("span",{className:"model-title__text"},u||a),p.createElement("img",{src:n(2517),height:"20px",width:"20px"}));const b=r.isOAS3()&&s.get("deprecated");switch(l=void 0!==l?l:!!v,y=s&&s.get("type")||y,y){case"object":return p.createElement(d,o()({className:"object"},this.props,{specPath:c,getConfigs:t,schema:s,name:a,deprecated:b,isRef:l,includeReadOnly:h,includeWriteOnly:f}));case"array":return p.createElement(m,o()({className:"array"},this.props,{getConfigs:t,schema:s,name:a,deprecated:b,required:i,includeReadOnly:h,includeWriteOnly:f}));default:return p.createElement(g,o()({},this.props,{getComponent:e,getConfigs:t,schema:s,name:a,deprecated:b,required:i}))}}}i()(P,"propTypes",{schema:u()(O()).isRequired,getComponent:A().func.isRequired,getConfigs:A().func.isRequired,specSelectors:A().object.isRequired,name:A().string,displayName:A().string,isRef:A().bool,required:A().bool,expandDepth:A().number,depth:A().number,specPath:O().list.isRequired,includeReadOnly:A().bool,includeWriteOnly:A().bool})},5623:(e,t,n)=>{"use strict";n.d(t,{Z:()=>h});var r=n(61125),o=n.n(r),s=n(28222),i=n.n(s),a=n(67294),l=n(84564),c=n.n(l),u=n(90242),p=n(27504);class h extends a.Component{constructor(e,t){super(e,t),o()(this,"getDefinitionUrl",(()=>{let{specSelectors:e}=this.props;return new(c())(e.url(),p.Z.location).toString()}));let{getConfigs:n}=e,{validatorUrl:r}=n();this.state={url:this.getDefinitionUrl(),validatorUrl:void 0===r?"https://validator.swagger.io/validator":r}}UNSAFE_componentWillReceiveProps(e){let{getConfigs:t}=e,{validatorUrl:n}=t();this.setState({url:this.getDefinitionUrl(),validatorUrl:void 0===n?"https://validator.swagger.io/validator":n})}render(){let{getConfigs:e}=this.props,{spec:t}=e(),n=(0,u.Nm)(this.state.validatorUrl);return"object"==typeof t&&i()(t).length?null:this.state.url&&(0,u.hW)(this.state.validatorUrl)&&(0,u.hW)(this.state.url)?a.createElement("span",{className:"float-right"},a.createElement("a",{target:"_blank",rel:"noopener noreferrer",href:`${n}/debug?url=${encodeURIComponent(this.state.url)}`},a.createElement(f,{src:`${n}?url=${encodeURIComponent(this.state.url)}`,alt:"Online validator badge"}))):null}}class f extends a.Component{constructor(e){super(e),this.state={loaded:!1,error:!1}}componentDidMount(){const e=new Image;e.onload=()=>{this.setState({loaded:!0})},e.onerror=()=>{this.setState({error:!0})},e.src=this.props.src}UNSAFE_componentWillReceiveProps(e){if(e.src!==this.props.src){const t=new Image;t.onload=()=>{this.setState({loaded:!0})},t.onerror=()=>{this.setState({error:!0})},t.src=e.src}}render(){return this.state.error?a.createElement("img",{alt:"Error"}):this.state.loaded?a.createElement("img",{src:this.props.src,alt:this.props.alt}):null}}},4599:(e,t,n)=>{"use strict";n.d(t,{Z:()=>ye,s:()=>ve});var r=n(67294),o=n(89927);function s(e,t){if(Array.prototype.indexOf)return e.indexOf(t);for(var n=0,r=e.length;n=0;n--)!0===t(e[n])&&e.splice(n,1)}function a(e){throw new Error("Unhandled case for value: '".concat(e,"'"))}var l=function(){function e(e){void 0===e&&(e={}),this.tagName="",this.attrs={},this.innerHTML="",this.whitespaceRegex=/\s+/,this.tagName=e.tagName||"",this.attrs=e.attrs||{},this.innerHTML=e.innerHtml||e.innerHTML||""}return e.prototype.setTagName=function(e){return this.tagName=e,this},e.prototype.getTagName=function(){return this.tagName||""},e.prototype.setAttr=function(e,t){return this.getAttrs()[e]=t,this},e.prototype.getAttr=function(e){return this.getAttrs()[e]},e.prototype.setAttrs=function(e){return Object.assign(this.getAttrs(),e),this},e.prototype.getAttrs=function(){return this.attrs||(this.attrs={})},e.prototype.setClass=function(e){return this.setAttr("class",e)},e.prototype.addClass=function(e){for(var t,n=this.getClass(),r=this.whitespaceRegex,o=n?n.split(r):[],i=e.split(r);t=i.shift();)-1===s(o,t)&&o.push(t);return this.getAttrs().class=o.join(" "),this},e.prototype.removeClass=function(e){for(var t,n=this.getClass(),r=this.whitespaceRegex,o=n?n.split(r):[],i=e.split(r);o.length&&(t=i.shift());){var a=s(o,t);-1!==a&&o.splice(a,1)}return this.getAttrs().class=o.join(" "),this},e.prototype.getClass=function(){return this.getAttrs().class||""},e.prototype.hasClass=function(e){return-1!==(" "+this.getClass()+" ").indexOf(" "+e+" ")},e.prototype.setInnerHTML=function(e){return this.innerHTML=e,this},e.prototype.setInnerHtml=function(e){return this.setInnerHTML(e)},e.prototype.getInnerHTML=function(){return this.innerHTML||""},e.prototype.getInnerHtml=function(){return this.getInnerHTML()},e.prototype.toAnchorString=function(){var e=this.getTagName(),t=this.buildAttrsStr();return["<",e,t=t?" "+t:"",">",this.getInnerHtml(),""].join("")},e.prototype.buildAttrsStr=function(){if(!this.attrs)return"";var e=this.getAttrs(),t=[];for(var n in e)e.hasOwnProperty(n)&&t.push(n+'="'+e[n]+'"');return t.join(" ")},e}();var c=function(){function e(e){void 0===e&&(e={}),this.newWindow=!1,this.truncate={},this.className="",this.newWindow=e.newWindow||!1,this.truncate=e.truncate||{},this.className=e.className||""}return e.prototype.build=function(e){return new l({tagName:"a",attrs:this.createAttrs(e),innerHtml:this.processAnchorText(e.getAnchorText())})},e.prototype.createAttrs=function(e){var t={href:e.getAnchorHref()},n=this.createCssClass(e);return n&&(t.class=n),this.newWindow&&(t.target="_blank",t.rel="noopener noreferrer"),this.truncate&&this.truncate.length&&this.truncate.length=a)return l.host.length==t?(l.host.substr(0,t-o)+n).substr(0,a+r):i(u,a).substr(0,a+r);var p="";if(l.path&&(p+="/"+l.path),l.query&&(p+="?"+l.query),p){if((u+p).length>=a)return(u+p).length==t?(u+p).substr(0,t):(u+i(p,a-u.length)).substr(0,a+r);u+=p}if(l.fragment){var h="#"+l.fragment;if((u+h).length>=a)return(u+h).length==t?(u+h).substr(0,t):(u+i(h,a-u.length)).substr(0,a+r);u+=h}if(l.scheme&&l.host){var f=l.scheme+"://";if((u+f).length0&&(d=u.substr(-1*Math.floor(a/2))),(u.substr(0,Math.ceil(a/2))+n+d).substr(0,a+r)}(e,n):"middle"===r?function(e,t,n){if(e.length<=t)return e;var r,o;null==n?(n="…",r=8,o=3):(r=n.length,o=n.length);var s=t-o,i="";return s>0&&(i=e.substr(-1*Math.floor(s/2))),(e.substr(0,Math.ceil(s/2))+n+i).substr(0,s+r)}(e,n):function(e,t,n){return function(e,t,n){var r;return e.length>t&&(null==n?(n="…",r=3):r=n.length,e=e.substring(0,t-r)+n),e}(e,t,n)}(e,n)},e}(),u=function(){function e(e){this.__jsduckDummyDocProp=null,this.matchedText="",this.offset=0,this.tagBuilder=e.tagBuilder,this.matchedText=e.matchedText,this.offset=e.offset}return e.prototype.getMatchedText=function(){return this.matchedText},e.prototype.setOffset=function(e){this.offset=e},e.prototype.getOffset=function(){return this.offset},e.prototype.getCssClassSuffixes=function(){return[this.getType()]},e.prototype.buildTag=function(){return this.tagBuilder.build(this)},e}(),p=function(e,t){return p=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},p(e,t)};function h(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function n(){this.constructor=e}p(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}var f=function(){return f=Object.assign||function(e){for(var t,n=1,r=arguments.length;n-1},e.isValidUriScheme=function(e){var t=e.match(this.uriSchemeRegex),n=t&&t[0].toLowerCase();return"javascript:"!==n&&"vbscript:"!==n},e.urlMatchDoesNotHaveProtocolOrDot=function(e,t){return!(!e||t&&this.hasFullProtocolRegex.test(t)||-1!==e.indexOf("."))},e.urlMatchDoesNotHaveAtLeastOneWordChar=function(e,t){return!(!e||!t)&&(!this.hasFullProtocolRegex.test(t)&&!this.hasWordCharAfterProtocolRegex.test(e))},e.hasFullProtocolRegex=/^[A-Za-z][-.+A-Za-z0-9]*:\/\//,e.uriSchemeRegex=/^[A-Za-z][-.+A-Za-z0-9]*:/,e.hasWordCharAfterProtocolRegex=new RegExp(":[^\\s]*?["+k+"]"),e.ipRegex=/[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?(:[0-9]*)?\/?$/,e}(),V=(d=new RegExp("[/?#](?:["+N+"\\-+&@#/%=~_()|'$*\\[\\]{}?!:,.;^✓]*["+N+"\\-+&@#/%=~_()|'$*\\[\\]{}✓])?"),new RegExp(["(?:","(",/(?:[A-Za-z][-.+A-Za-z0-9]{0,63}:(?![A-Za-z][-.+A-Za-z0-9]{0,63}:\/\/)(?!\d+\/?)(?:\/\/)?)/.source,D(2),")","|","(","(//)?",/(?:www\.)/.source,D(6),")","|","(","(//)?",D(10)+"\\.",L.source,"(?![-"+P+"])",")",")","(?::[0-9]+)?","(?:"+d.source+")?"].join(""),"gi")),W=new RegExp("["+N+"]"),J=function(e){function t(t){var n=e.call(this,t)||this;return n.stripPrefix={scheme:!0,www:!0},n.stripTrailingSlash=!0,n.decodePercentEncoding=!0,n.matcherRegex=V,n.wordCharRegExp=W,n.stripPrefix=t.stripPrefix,n.stripTrailingSlash=t.stripTrailingSlash,n.decodePercentEncoding=t.decodePercentEncoding,n}return h(t,e),t.prototype.parseMatches=function(e){for(var t,n=this.matcherRegex,r=this.stripPrefix,o=this.stripTrailingSlash,s=this.decodePercentEncoding,i=this.tagBuilder,a=[],l=function(){var n=t[0],l=t[1],u=t[4],p=t[5],h=t[9],f=t.index,d=p||h,m=e.charAt(f-1);if(!z.isValid(n,l))return"continue";if(f>0&&"@"===m)return"continue";if(f>0&&d&&c.wordCharRegExp.test(m))return"continue";if(/\?$/.test(n)&&(n=n.substr(0,n.length-1)),c.matchHasUnbalancedClosingParen(n))n=n.substr(0,n.length-1);else{var g=c.matchHasInvalidCharAfterTld(n,l);g>-1&&(n=n.substr(0,g))}var y=["http://","https://"].find((function(e){return!!l&&-1!==l.indexOf(e)}));if(y){var v=n.indexOf(y);n=n.substr(v),l=l.substr(v),f+=v}var w=l?"scheme":u?"www":"tld",E=!!l;a.push(new b({tagBuilder:i,matchedText:n,offset:f,urlMatchType:w,url:n,protocolUrlMatch:E,protocolRelativeMatch:!!d,stripPrefix:r,stripTrailingSlash:o,decodePercentEncoding:s}))},c=this;null!==(t=n.exec(e));)l();return a},t.prototype.matchHasUnbalancedClosingParen=function(e){var t,n=e.charAt(e.length-1);if(")"===n)t="(";else if("]"===n)t="[";else{if("}"!==n)return!1;t="{"}for(var r=0,o=0,s=e.length-1;o-1&&s-i<=140){var o=e.slice(i,s),a=new g({tagBuilder:t,matchedText:o,offset:i,serviceName:n,hashtag:o.slice(1)});r.push(a)}}},t}(w),G=["twitter","facebook","instagram","tiktok"],Z=new RegExp("".concat(/(?:(?:(?:(\+)?\d{1,3}[-\040.]?)?\(?\d{3}\)?[-\040.]?\d{3}[-\040.]?\d{4})|(?:(\+)(?:9[976]\d|8[987530]\d|6[987]\d|5[90]\d|42\d|3[875]\d|2[98654321]\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|4[987654310]|3[9643210]|2[70]|7|1)[-\040.]?(?:\d[-\040.]?){6,12}\d+))([,;]+[0-9]+#?)*/.source,"|").concat(/(0([1-9]{1}-?[1-9]\d{3}|[1-9]{2}-?\d{3}|[1-9]{2}\d{1}-?\d{2}|[1-9]{2}\d{2}-?\d{1})-?\d{4}|0[789]0-?\d{4}-?\d{4}|050-?\d{4}-?\d{4})/.source),"g"),Y=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.matcherRegex=Z,t}return h(t,e),t.prototype.parseMatches=function(e){for(var t,n=this.matcherRegex,r=this.tagBuilder,o=[];null!==(t=n.exec(e));){var s=t[0],i=s.replace(/[^0-9,;#]/g,""),a=!(!t[1]&&!t[2]),l=0==t.index?"":e.substr(t.index-1,1),c=e.substr(t.index+s.length,1),u=!l.match(/\d/)&&!c.match(/\d/);this.testMatch(t[3])&&this.testMatch(s)&&u&&o.push(new v({tagBuilder:r,matchedText:s,offset:t.index,number:i,plusSign:a}))}return o},t.prototype.testMatch=function(e){return S.test(e)},t}(w),X=new RegExp("@[_".concat(N,"]{1,50}(?![_").concat(N,"])"),"g"),Q=new RegExp("@[_.".concat(N,"]{1,30}(?![_").concat(N,"])"),"g"),ee=new RegExp("@[-_.".concat(N,"]{1,50}(?![-_").concat(N,"])"),"g"),te=new RegExp("@[_.".concat(N,"]{1,23}[_").concat(N,"](?![_").concat(N,"])"),"g"),ne=new RegExp("[^"+N+"]"),re=function(e){function t(t){var n=e.call(this,t)||this;return n.serviceName="twitter",n.matcherRegexes={twitter:X,instagram:Q,soundcloud:ee,tiktok:te},n.nonWordCharRegex=ne,n.serviceName=t.serviceName,n}return h(t,e),t.prototype.parseMatches=function(e){var t,n=this.serviceName,r=this.matcherRegexes[this.serviceName],o=this.nonWordCharRegex,s=this.tagBuilder,i=[];if(!r)return i;for(;null!==(t=r.exec(e));){var a=t.index,l=e.charAt(a-1);if(0===a||o.test(l)){var c=t[0].replace(/\.+$/g,""),u=c.slice(1);i.push(new y({tagBuilder:s,matchedText:c,offset:a,serviceName:n,mention:u}))}}return i},t}(w);function oe(e,t){for(var n,r=t.onOpenTag,o=t.onCloseTag,s=t.onText,i=t.onComment,l=t.onDoctype,c=new se,u=0,p=e.length,h=0,d=0,m=c;u"===e?(m=new se(f(f({},m),{name:J()})),W()):E.test(e)||x.test(e)||":"===e||z()}function w(e){">"===e?z():E.test(e)?h=3:z()}function S(e){_.test(e)||("/"===e?h=12:">"===e?W():"<"===e?V():"="===e||j.test(e)||O.test(e)?z():h=5)}function k(e){_.test(e)?h=6:"/"===e?h=12:"="===e?h=7:">"===e?W():"<"===e?V():j.test(e)&&z()}function A(e){_.test(e)||("/"===e?h=12:"="===e?h=7:">"===e?W():"<"===e?V():j.test(e)?z():h=5)}function C(e){_.test(e)||('"'===e?h=8:"'"===e?h=9:/[>=`]/.test(e)?z():"<"===e?V():h=10)}function P(e){'"'===e&&(h=11)}function N(e){"'"===e&&(h=11)}function I(e){_.test(e)?h=4:">"===e?W():"<"===e&&V()}function T(e){_.test(e)?h=4:"/"===e?h=12:">"===e?W():"<"===e?V():(h=4,u--)}function R(e){">"===e?(m=new se(f(f({},m),{isClosing:!0})),W()):h=4}function M(t){"--"===e.substr(u,2)?(u+=2,m=new se(f(f({},m),{type:"comment"})),h=14):"DOCTYPE"===e.substr(u,7).toUpperCase()?(u+=7,m=new se(f(f({},m),{type:"doctype"})),h=20):z()}function D(e){"-"===e?h=15:">"===e?z():h=16}function F(e){"-"===e?h=18:">"===e?z():h=16}function L(e){"-"===e&&(h=17)}function B(e){h="-"===e?18:16}function $(e){">"===e?W():"!"===e?h=19:"-"===e||(h=16)}function q(e){"-"===e?h=17:">"===e?W():h=16}function U(e){">"===e?W():"<"===e&&V()}function z(){h=0,m=c}function V(){h=1,m=new se({idx:u})}function W(){var t=e.slice(d,m.idx);t&&s(t,d),"comment"===m.type?i(m.idx):"doctype"===m.type?l(m.idx):(m.isOpening&&r(m.name,m.idx),m.isClosing&&o(m.name,m.idx)),z(),d=u+1}function J(){var t=m.idx+(m.isClosing?2:1);return e.slice(t,u).toLowerCase()}d=0&&r++},onText:function(e,n){if(0===r){var s=function(e,t){if(!t.global)throw new Error("`splitRegex` must have the 'g' flag set");for(var n,r=[],o=0;n=t.exec(e);)r.push(e.substring(o,n.index)),r.push(n[0]),o=n.index+n[0].length;return r.push(e.substring(o)),r}(e,/( | |<|<|>|>|"|"|')/gi),i=n;s.forEach((function(e,n){if(n%2==0){var r=t.parseText(e,i);o.push.apply(o,r)}i+=e.length}))}},onCloseTag:function(e){n.indexOf(e)>=0&&(r=Math.max(r-1,0))},onComment:function(e){},onDoctype:function(e){}}),o=this.compactMatches(o),o=this.removeUnwantedMatches(o)},e.prototype.compactMatches=function(e){e.sort((function(e,t){return e.getOffset()-t.getOffset()}));for(var t=0;to?t:t+1;e.splice(i,1);continue}if(e[t+1].getOffset()/g,">"));for(var t=this.parse(e),n=[],r=0,o=0,s=t.length;o/i.test(e)}function ce(){var e=[],t=new ie({stripPrefix:!1,url:!0,email:!0,replaceFn:function(t){switch(t.getType()){case"url":e.push({text:t.matchedText,url:t.getUrl()});break;case"email":e.push({text:t.matchedText,url:"mailto:"+t.getEmail().replace(/^mailto:/i,"")})}return!1}});return{links:e,autolinker:t}}function ue(e){var t,n,r,o,s,i,a,l,c,u,p,h,f,d,m=e.tokens,g=null;for(n=0,r=m.length;n=0;t--)if("link_close"!==(s=o[t]).type){if("htmltag"===s.type&&(d=s.content,/^\s]/i.test(d)&&p>0&&p--,le(s.content)&&p++),!(p>0)&&"text"===s.type&&ae.test(s.content)){if(g||(h=(g=ce()).links,f=g.autolinker),i=s.content,h.length=0,f.link(i),!h.length)continue;for(a=[],u=s.level,l=0;l({useUnsafeMarkdown:!1})};const ye=ge;function ve(e){let{useUnsafeMarkdown:t=!1}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const n=t,r=t?[]:["style","class"];return t&&!ve.hasWarnedAboutDeprecation&&(console.warn("useUnsafeMarkdown display configuration parameter is deprecated since >3.26.0 and will be removed in v4.0.0."),ve.hasWarnedAboutDeprecation=!0),fe().sanitize(e,{ADD_ATTR:["target"],FORBID_TAGS:["style","form"],ALLOW_DATA_ATTR:n,FORBID_ATTR:r})}ve.hasWarnedAboutDeprecation=!1},45308:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>h});var r,o=n(86),s=n.n(o),i=n(8712),a=n.n(i),l=n(90242),c=n(27621);const u=n(95102),p={},h=p;s()(r=a()(u).call(u)).call(r,(function(e){if("./index.js"===e)return;let t=u(e);p[(0,l.Zl)(e)]=t.default?t.default:t})),p.SafeRender=c.default},55812:(e,t,n)=>{"use strict";n.r(t),n.d(t,{AUTHORIZE:()=>h,AUTHORIZE_OAUTH2:()=>m,CONFIGURE_AUTH:()=>y,LOGOUT:()=>f,PRE_AUTHORIZE_OAUTH2:()=>d,RESTORE_AUTHORIZATION:()=>v,SHOW_AUTH_POPUP:()=>p,VALIDATE:()=>g,authPopup:()=>M,authorize:()=>w,authorizeAccessCodeWithBasicAuthentication:()=>P,authorizeAccessCodeWithFormParams:()=>C,authorizeApplication:()=>A,authorizeOauth2:()=>j,authorizeOauth2WithPersistOption:()=>O,authorizePassword:()=>k,authorizeRequest:()=>N,authorizeWithPersistOption:()=>E,configureAuth:()=>I,logout:()=>x,logoutWithPersistOption:()=>S,persistAuthorizationIfNeeded:()=>R,preAuthorizeImplicit:()=>_,restoreAuthorization:()=>T,showDefinitions:()=>b});var r=n(35627),o=n.n(r),s=n(76986),i=n.n(s),a=n(84564),l=n.n(a),c=n(27504),u=n(90242);const p="show_popup",h="authorize",f="logout",d="pre_authorize_oauth2",m="authorize_oauth2",g="validate",y="configure_auth",v="restore_authorization";function b(e){return{type:p,payload:e}}function w(e){return{type:h,payload:e}}const E=e=>t=>{let{authActions:n}=t;n.authorize(e),n.persistAuthorizationIfNeeded()};function x(e){return{type:f,payload:e}}const S=e=>t=>{let{authActions:n}=t;n.logout(e),n.persistAuthorizationIfNeeded()},_=e=>t=>{let{authActions:n,errActions:r}=t,{auth:s,token:i,isValid:a}=e,{schema:l,name:u}=s,p=l.get("flow");delete c.Z.swaggerUIRedirectOauth2,"accessCode"===p||a||r.newAuthErr({authId:u,source:"auth",level:"warning",message:"Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"}),i.error?r.newAuthErr({authId:u,source:"auth",level:"error",message:o()(i)}):n.authorizeOauth2WithPersistOption({auth:s,token:i})};function j(e){return{type:m,payload:e}}const O=e=>t=>{let{authActions:n}=t;n.authorizeOauth2(e),n.persistAuthorizationIfNeeded()},k=e=>t=>{let{authActions:n}=t,{schema:r,name:o,username:s,password:a,passwordType:l,clientId:c,clientSecret:p}=e,h={grant_type:"password",scope:e.scopes.join(" "),username:s,password:a},f={};switch(l){case"request-body":!function(e,t,n){t&&i()(e,{client_id:t});n&&i()(e,{client_secret:n})}(h,c,p);break;case"basic":f.Authorization="Basic "+(0,u.r3)(c+":"+p);break;default:console.warn(`Warning: invalid passwordType ${l} was passed, not including client id and secret`)}return n.authorizeRequest({body:(0,u.GZ)(h),url:r.get("tokenUrl"),name:o,headers:f,query:{},auth:e})};const A=e=>t=>{let{authActions:n}=t,{schema:r,scopes:o,name:s,clientId:i,clientSecret:a}=e,l={Authorization:"Basic "+(0,u.r3)(i+":"+a)},c={grant_type:"client_credentials",scope:o.join(" ")};return n.authorizeRequest({body:(0,u.GZ)(c),name:s,url:r.get("tokenUrl"),auth:e,headers:l})},C=e=>{let{auth:t,redirectUrl:n}=e;return e=>{let{authActions:r}=e,{schema:o,name:s,clientId:i,clientSecret:a,codeVerifier:l}=t,c={grant_type:"authorization_code",code:t.code,client_id:i,client_secret:a,redirect_uri:n,code_verifier:l};return r.authorizeRequest({body:(0,u.GZ)(c),name:s,url:o.get("tokenUrl"),auth:t})}},P=e=>{let{auth:t,redirectUrl:n}=e;return e=>{let{authActions:r}=e,{schema:o,name:s,clientId:i,clientSecret:a,codeVerifier:l}=t,c={Authorization:"Basic "+(0,u.r3)(i+":"+a)},p={grant_type:"authorization_code",code:t.code,client_id:i,redirect_uri:n,code_verifier:l};return r.authorizeRequest({body:(0,u.GZ)(p),name:s,url:o.get("tokenUrl"),auth:t,headers:c})}},N=e=>t=>{let n,{fn:r,getConfigs:s,authActions:a,errActions:c,oas3Selectors:u,specSelectors:p,authSelectors:h}=t,{body:f,query:d={},headers:m={},name:g,url:y,auth:v}=e,{additionalQueryStringParams:b}=h.getConfigs()||{};if(p.isOAS3()){let e=u.serverEffectiveValue(u.selectedServer());n=l()(y,e,!0)}else n=l()(y,p.url(),!0);"object"==typeof b&&(n.query=i()({},n.query,b));const w=n.toString();let E=i()({Accept:"application/json, text/plain, */*","Content-Type":"application/x-www-form-urlencoded","X-Requested-With":"XMLHttpRequest"},m);r.fetch({url:w,method:"post",headers:E,query:d,body:f,requestInterceptor:s().requestInterceptor,responseInterceptor:s().responseInterceptor}).then((function(e){let t=JSON.parse(e.data),n=t&&(t.error||""),r=t&&(t.parseError||"");e.ok?n||r?c.newAuthErr({authId:g,level:"error",source:"auth",message:o()(t)}):a.authorizeOauth2WithPersistOption({auth:v,token:t}):c.newAuthErr({authId:g,level:"error",source:"auth",message:e.statusText})})).catch((e=>{let t=new Error(e).message;if(e.response&&e.response.data){const n=e.response.data;try{const e="string"==typeof n?JSON.parse(n):n;e.error&&(t+=`, error: ${e.error}`),e.error_description&&(t+=`, description: ${e.error_description}`)}catch(e){}}c.newAuthErr({authId:g,level:"error",source:"auth",message:t})}))};function I(e){return{type:y,payload:e}}function T(e){return{type:v,payload:e}}const R=()=>e=>{let{authSelectors:t,getConfigs:n}=e;if(!n().persistAuthorization)return;const r=t.authorized().toJS();localStorage.setItem("authorized",o()(r))},M=(e,t)=>()=>{c.Z.swaggerUIRedirectOauth2=t,c.Z.open(e)}},53779:(e,t,n)=>{"use strict";n.r(t),n.d(t,{loaded:()=>r});const r=(e,t)=>n=>{const{getConfigs:r,authActions:o}=t,s=r();if(e(n),s.persistAuthorization){const e=localStorage.getItem("authorized");e&&o.restoreAuthorization({authorized:JSON.parse(e)})}}},93705:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>p,preauthorizeApiKey:()=>f,preauthorizeBasic:()=>h});var r=n(11189),o=n.n(r),s=n(43962),i=n(55812),a=n(60035),l=n(60489),c=n(53779),u=n(22849);function p(){return{afterLoad(e){this.rootInjects=this.rootInjects||{},this.rootInjects.initOAuth=e.authActions.configureAuth,this.rootInjects.preauthorizeApiKey=o()(f).call(f,null,e),this.rootInjects.preauthorizeBasic=o()(h).call(h,null,e)},statePlugins:{auth:{reducers:s.default,actions:i,selectors:a,wrapActions:{authorize:u.authorize,logout:u.logout}},configs:{wrapActions:{loaded:c.loaded}},spec:{wrapActions:{execute:l.execute}}}}}function h(e,t,n,r){const{authActions:{authorize:o},specSelectors:{specJson:s,isOAS3:i}}=e,a=i()?["components","securitySchemes"]:["securityDefinitions"],l=s().getIn([...a,t]);return l?o({[t]:{value:{username:n,password:r},schema:l.toJS()}}):null}function f(e,t,n){const{authActions:{authorize:r},specSelectors:{specJson:o,isOAS3:s}}=e,i=s()?["components","securitySchemes"]:["securityDefinitions"],a=o().getIn([...i,t]);return a?r({[t]:{value:n,schema:a.toJS()}}):null}},43962:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>u});var r=n(86),o=n.n(r),s=n(76986),i=n.n(s),a=n(43393),l=n(90242),c=n(55812);const u={[c.SHOW_AUTH_POPUP]:(e,t)=>{let{payload:n}=t;return e.set("showDefinitions",n)},[c.AUTHORIZE]:(e,t)=>{var n;let{payload:r}=t,s=(0,a.fromJS)(r),i=e.get("authorized")||(0,a.Map)();return o()(n=s.entrySeq()).call(n,(t=>{let[n,r]=t;if(!(0,l.Wl)(r.getIn))return e.set("authorized",i);let o=r.getIn(["schema","type"]);if("apiKey"===o||"http"===o)i=i.set(n,r);else if("basic"===o){let e=r.getIn(["value","username"]),t=r.getIn(["value","password"]);i=i.setIn([n,"value"],{username:e,header:"Basic "+(0,l.r3)(e+":"+t)}),i=i.setIn([n,"schema"],r.get("schema"))}})),e.set("authorized",i)},[c.AUTHORIZE_OAUTH2]:(e,t)=>{let n,{payload:r}=t,{auth:o,token:s}=r;o.token=i()({},s),n=(0,a.fromJS)(o);let l=e.get("authorized")||(0,a.Map)();return l=l.set(n.get("name"),n),e.set("authorized",l)},[c.LOGOUT]:(e,t)=>{let{payload:n}=t,r=e.get("authorized").withMutations((e=>{o()(n).call(n,(t=>{e.delete(t)}))}));return e.set("authorized",r)},[c.CONFIGURE_AUTH]:(e,t)=>{let{payload:n}=t;return e.set("configs",n)},[c.RESTORE_AUTHORIZATION]:(e,t)=>{let{payload:n}=t;return e.set("authorized",(0,a.fromJS)(n.authorized))}}},60035:(e,t,n)=>{"use strict";n.r(t),n.d(t,{authorized:()=>x,definitionsForRequirements:()=>E,definitionsToAuthorize:()=>b,getConfigs:()=>_,getDefinitionsByNames:()=>w,isAuthorized:()=>S,shownDefinitions:()=>v});var r=n(86),o=n.n(r),s=n(51679),i=n.n(s),a=n(14418),l=n.n(a),c=n(11882),u=n.n(c),p=n(97606),h=n.n(p),f=n(28222),d=n.n(f),m=n(20573),g=n(43393);const y=e=>e,v=(0,m.P1)(y,(e=>e.get("showDefinitions"))),b=(0,m.P1)(y,(()=>e=>{var t;let{specSelectors:n}=e,r=n.securityDefinitions()||(0,g.Map)({}),s=(0,g.List)();return o()(t=r.entrySeq()).call(t,(e=>{let[t,n]=e,r=(0,g.Map)();r=r.set(t,n),s=s.push(r)})),s})),w=(e,t)=>e=>{var n;let{specSelectors:r}=e;console.warn("WARNING: getDefinitionsByNames is deprecated and will be removed in the next major version.");let s=r.securityDefinitions(),i=(0,g.List)();return o()(n=t.valueSeq()).call(n,(e=>{var t;let n=(0,g.Map)();o()(t=e.entrySeq()).call(t,(e=>{let t,[r,i]=e,a=s.get(r);var l;"oauth2"===a.get("type")&&i.size&&(t=a.get("scopes"),o()(l=t.keySeq()).call(l,(e=>{i.contains(e)||(t=t.delete(e))})),a=a.set("allowedScopes",t));n=n.set(r,a)})),i=i.push(n)})),i},E=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:(0,g.List)();return e=>{let{authSelectors:n}=e;const r=n.definitionsToAuthorize()||(0,g.List)();let s=(0,g.List)();return o()(r).call(r,(e=>{let n=i()(t).call(t,(t=>t.get(e.keySeq().first())));n&&(o()(e).call(e,((t,r)=>{if("oauth2"===t.get("type")){const i=n.get(r);let a=t.get("scopes");var s;if(g.List.isList(i)&&g.Map.isMap(a))o()(s=a.keySeq()).call(s,(e=>{i.contains(e)||(a=a.delete(e))})),e=e.set(r,t.set("scopes",a))}})),s=s.push(e))})),s}},x=(0,m.P1)(y,(e=>e.get("authorized")||(0,g.Map)())),S=(e,t)=>e=>{var n;let{authSelectors:r}=e,o=r.authorized();return g.List.isList(t)?!!l()(n=t.toJS()).call(n,(e=>{var t,n;return-1===u()(t=h()(n=d()(e)).call(n,(e=>!!o.get(e)))).call(t,!1)})).length:null},_=(0,m.P1)(y,(e=>e.get("configs")))},60489:(e,t,n)=>{"use strict";n.r(t),n.d(t,{execute:()=>r});const r=(e,t)=>{let{authSelectors:n,specSelectors:r}=t;return t=>{let{path:o,method:s,operation:i,extras:a}=t,l={authorized:n.authorized()&&n.authorized().toJS(),definitions:r.securityDefinitions()&&r.securityDefinitions().toJS(),specSecurity:r.security()&&r.security().toJS()};return e({path:o,method:s,operation:i,securities:l,...a})}}},22849:(e,t,n)=>{"use strict";n.r(t),n.d(t,{authorize:()=>c,logout:()=>u});var r=n(3665),o=n.n(r),s=n(58309),i=n.n(s),a=n(86),l=n.n(a);const c=(e,t)=>n=>{e(n);if(t.getConfigs().persistAuthorization)try{const[{schema:e,value:t}]=o()(n),r="apiKey"===e.get("type"),s="cookie"===e.get("in");r&&s&&(document.cookie=`${e.get("name")}=${t}; SameSite=None; Secure`)}catch(e){console.error("Error persisting cookie based apiKey in document.cookie.",e)}},u=(e,t)=>n=>{const r=t.getConfigs(),o=t.authSelectors.authorized();try{r.persistAuthorization&&i()(n)&&l()(n).call(n,(e=>{const t=o.get(e,{}),n="apiKey"===t.getIn(["schema","type"]),r="cookie"===t.getIn(["schema","in"]);if(n&&r){const e=t.getIn(["schema","name"]);document.cookie=`${e}=; Max-Age=-99999999`}}))}catch(e){console.error("Error deleting cookie based apiKey from document.cookie.",e)}e(n)}},70714:(e,t,n)=>{"use strict";n.r(t),n.d(t,{TOGGLE_CONFIGS:()=>o,UPDATE_CONFIGS:()=>r,loaded:()=>a,toggle:()=>i,update:()=>s});const r="configs_update",o="configs_toggle";function s(e,t){return{type:r,payload:{[e]:t}}}function i(e){return{type:o,payload:e}}const a=()=>()=>{}},92256:(e,t,n)=>{"use strict";n.r(t),n.d(t,{parseYamlConfig:()=>o});var r=n(1272);const o=(e,t)=>{try{return r.ZP.load(e)}catch(e){return t&&t.errActions.newThrownErr(new Error(e)),{}}}},46709:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>c});var r=n(92256),o=n(70714),s=n(22698),i=n(69018),a=n(37743);const l={getLocalConfig:()=>(0,r.parseYamlConfig)('---\nurl: "https://petstore.swagger.io/v2/swagger.json"\ndom_id: "#swagger-ui"\nvalidatorUrl: "https://validator.swagger.io/validator"\n')};function c(){return{statePlugins:{spec:{actions:s,selectors:l},configs:{reducers:a.default,actions:o,selectors:i}}}}},37743:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});var r=n(43393),o=n(70714);const s={[o.UPDATE_CONFIGS]:(e,t)=>e.merge((0,r.fromJS)(t.payload)),[o.TOGGLE_CONFIGS]:(e,t)=>{const n=t.payload,r=e.get(n);return e.set(n,!r)}}},69018:(e,t,n)=>{"use strict";n.r(t),n.d(t,{get:()=>s});var r=n(58309),o=n.n(r);const s=(e,t)=>e.getIn(o()(t)?t:[t])},22698:(e,t,n)=>{"use strict";n.r(t),n.d(t,{downloadConfig:()=>o,getConfigByUrl:()=>s});var r=n(92256);const o=e=>t=>{const{fn:{fetch:n}}=t;return n(e)},s=(e,t)=>n=>{let{specActions:o}=n;if(e)return o.downloadConfig(e).then(s,s);function s(n){n instanceof Error||n.status>=400?(o.updateLoadingStatus("failedConfig"),o.updateLoadingStatus("failedConfig"),o.updateUrl(""),console.error(n.statusText+" "+e.url),t(null)):t((0,r.parseYamlConfig)(n.text))}}},31970:(e,t,n)=>{"use strict";n.r(t),n.d(t,{setHash:()=>r});const r=e=>e?history.pushState(null,null,`#${e}`):window.location.hash=""},34980:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>i});var r=n(41599),o=n(60877),s=n(34584);function i(){return[r.default,{statePlugins:{configs:{wrapActions:{loaded:(e,t)=>function(){e(...arguments);const n=decodeURIComponent(window.location.hash);t.layoutActions.parseDeepLinkHash(n)}}}},wrapComponents:{operation:o.default,OperationTag:s.default}}]}},41599:(e,t,n)=>{"use strict";n.r(t),n.d(t,{clearScrollTo:()=>_,default:()=>j,parseDeepLinkHash:()=>E,readyToScroll:()=>x,scrollTo:()=>w,scrollToElement:()=>S,show:()=>b});var r=n(58309),o=n.n(r),s=n(24278),i=n.n(s),a=n(97606),l=n.n(a),c=n(11882),u=n.n(c),p=n(31970),h=n(45172),f=n.n(h),d=n(90242),m=n(43393),g=n.n(m);const y="layout_scroll_to",v="layout_clear_scroll",b=(e,t)=>{let{getConfigs:n,layoutSelectors:r}=t;return function(){for(var t=arguments.length,s=new Array(t),i=0;i({type:y,payload:o()(e)?e:[e]}),E=e=>t=>{let{layoutActions:n,layoutSelectors:r,getConfigs:o}=t;if(o().deepLinking&&e){var s;let t=i()(e).call(e,1);"!"===t[0]&&(t=i()(t).call(t,1)),"/"===t[0]&&(t=i()(t).call(t,1));const o=l()(s=t.split("/")).call(s,(e=>e||"")),a=r.isShownKeyFromUrlHashArray(o),[c,p="",h=""]=a;if("operations"===c){const e=r.isShownKeyFromUrlHashArray([p]);u()(p).call(p,"_")>-1&&(console.warn("Warning: escaping deep link whitespace with `_` will be unsupported in v4.0, use `%20` instead."),n.show(l()(e).call(e,(e=>e.replace(/_/g," "))),!0)),n.show(e,!0)}(u()(p).call(p,"_")>-1||u()(h).call(h,"_")>-1)&&(console.warn("Warning: escaping deep link whitespace with `_` will be unsupported in v4.0, use `%20` instead."),n.show(l()(a).call(a,(e=>e.replace(/_/g," "))),!0)),n.show(a,!0),n.scrollTo(a)}},x=(e,t)=>n=>{const r=n.layoutSelectors.getScrollToKey();g().is(r,(0,m.fromJS)(e))&&(n.layoutActions.scrollToElement(t),n.layoutActions.clearScrollTo())},S=(e,t)=>n=>{try{t=t||n.fn.getScrollParent(e),f().createScroller(t).to(e)}catch(e){console.error(e)}},_=()=>({type:v});const j={fn:{getScrollParent:function(e,t){const n=document.documentElement;let r=getComputedStyle(e);const o="absolute"===r.position,s=t?/(auto|scroll|hidden)/:/(auto|scroll)/;if("fixed"===r.position)return n;for(let t=e;t=t.parentElement;)if(r=getComputedStyle(t),(!o||"static"!==r.position)&&s.test(r.overflow+r.overflowY+r.overflowX))return t;return n}},statePlugins:{layout:{actions:{scrollToElement:S,scrollTo:w,clearScrollTo:_,readyToScroll:x,parseDeepLinkHash:E},selectors:{getScrollToKey:e=>e.get("scrollToKey"),isShownKeyFromUrlHashArray(e,t){const[n,r]=t;return r?["operations",n,r]:n?["operations-tag",n]:[]},urlHashArrayFromIsShownKey(e,t){let[n,r,o]=t;return"operations"==n?[r,o]:"operations-tag"==n?[r]:[]}},reducers:{[y]:(e,t)=>e.set("scrollToKey",g().fromJS(t.payload)),[v]:e=>e.delete("scrollToKey")},wrapActions:{show:b}}}}},34584:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>i});var r=n(61125),o=n.n(r),s=n(67294);const i=(e,t)=>class extends s.Component{constructor(){super(...arguments),o()(this,"onLoad",(e=>{const{tag:n}=this.props,r=["operations-tag",n];t.layoutActions.readyToScroll(r,e)}))}render(){return s.createElement("span",{ref:this.onLoad},s.createElement(e,this.props))}}},60877:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>i});var r=n(61125),o=n.n(r),s=n(67294);n(23930);const i=(e,t)=>class extends s.Component{constructor(){super(...arguments),o()(this,"onLoad",(e=>{const{operation:n}=this.props,{tag:r,operationId:o}=n.toObject();let{isShownKey:s}=n.toObject();s=s||["operations",r,o],t.layoutActions.readyToScroll(s,e)}))}render(){return s.createElement("span",{ref:this.onLoad},s.createElement(e,this.props))}}},48011:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>d});var r=n(76986),o=n.n(r),s=n(63460),i=n.n(s),a=n(11882),l=n.n(a),c=n(35627),u=n.n(c),p=n(20573),h=n(43393),f=n(27504);function d(e){let{fn:t}=e;return{statePlugins:{spec:{actions:{download:e=>n=>{let{errActions:r,specSelectors:s,specActions:a,getConfigs:l}=n,{fetch:c}=t;const u=l();function p(t){if(t instanceof Error||t.status>=400)return a.updateLoadingStatus("failed"),r.newThrownErr(o()(new Error((t.message||t.statusText)+" "+e),{source:"fetch"})),void(!t.status&&t instanceof Error&&function(){try{let t;if("URL"in f.Z?t=new(i())(e):(t=document.createElement("a"),t.href=e),"https:"!==t.protocol&&"https:"===f.Z.location.protocol){const e=o()(new Error(`Possible mixed-content issue? The page was loaded over https:// but a ${t.protocol}// URL was specified. Check that you are not attempting to load mixed content.`),{source:"fetch"});return void r.newThrownErr(e)}if(t.origin!==f.Z.location.origin){const e=o()(new Error(`Possible cross-origin (CORS) issue? The URL origin (${t.origin}) does not match the page (${f.Z.location.origin}). Check the server returns the correct 'Access-Control-Allow-*' headers.`),{source:"fetch"});r.newThrownErr(e)}}catch(e){return}}());a.updateLoadingStatus("success"),a.updateSpec(t.text),s.url()!==e&&a.updateUrl(e)}e=e||s.url(),a.updateLoadingStatus("loading"),r.clear({source:"fetch"}),c({url:e,loadSpec:!0,requestInterceptor:u.requestInterceptor||(e=>e),responseInterceptor:u.responseInterceptor||(e=>e),credentials:"same-origin",headers:{Accept:"application/json,*/*"}}).then(p,p)},updateLoadingStatus:e=>{let t=[null,"loading","failed","success","failedConfig"];return-1===l()(t).call(t,e)&&console.error(`Error: ${e} is not one of ${u()(t)}`),{type:"spec_update_loading_status",payload:e}}},reducers:{spec_update_loading_status:(e,t)=>"string"==typeof t.payload?e.set("loadingStatus",t.payload):e},selectors:{loadingStatus:(0,p.P1)((e=>e||(0,h.Map)()),(e=>e.get("loadingStatus")||null))}}}}}},34966:(e,t,n)=>{"use strict";n.r(t),n.d(t,{CLEAR:()=>c,CLEAR_BY:()=>u,NEW_AUTH_ERR:()=>l,NEW_SPEC_ERR:()=>i,NEW_SPEC_ERR_BATCH:()=>a,NEW_THROWN_ERR:()=>o,NEW_THROWN_ERR_BATCH:()=>s,clear:()=>g,clearBy:()=>y,newAuthErr:()=>m,newSpecErr:()=>f,newSpecErrBatch:()=>d,newThrownErr:()=>p,newThrownErrBatch:()=>h});var r=n(7710);const o="err_new_thrown_err",s="err_new_thrown_err_batch",i="err_new_spec_err",a="err_new_spec_err_batch",l="err_new_auth_err",c="err_clear",u="err_clear_by";function p(e){return{type:o,payload:(0,r.serializeError)(e)}}function h(e){return{type:s,payload:e}}function f(e){return{type:i,payload:e}}function d(e){return{type:a,payload:e}}function m(e){return{type:l,payload:e}}function g(){return{type:c,payload:arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}}}function y(){return{type:u,payload:arguments.length>0&&void 0!==arguments[0]?arguments[0]:()=>!0}}},56982:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>u});var r=n(14418),o=n.n(r),s=n(97606),i=n.n(s),a=n(54061),l=n.n(a);const c=[n(2392),n(21835)];function u(e){var t;let n={jsSpec:{}},r=l()(c,((e,t)=>{try{let r=t.transform(e,n);return o()(r).call(r,(e=>!!e))}catch(t){return console.error("Transformer error:",t),e}}),e);return i()(t=o()(r).call(r,(e=>!!e))).call(t,(e=>(!e.get("line")&&e.get("path"),e)))}},2392:(e,t,n)=>{"use strict";n.r(t),n.d(t,{transform:()=>p});var r=n(97606),o=n.n(r),s=n(11882),i=n.n(s),a=n(24278),l=n.n(a),c=n(24282),u=n.n(c);function p(e){return o()(e).call(e,(e=>{var t;let n="is not of a type(s)",r=i()(t=e.get("message")).call(t,n);if(r>-1){var o,s;let t=l()(o=e.get("message")).call(o,r+19).split(",");return e.set("message",l()(s=e.get("message")).call(s,0,r)+function(e){return u()(e).call(e,((e,t,n,r)=>n===r.length-1&&r.length>1?e+"or "+t:r[n+1]&&r.length>2?e+t+", ":r[n+1]?e+t+" ":e+t),"should be a")}(t))}return e}))}},21835:(e,t,n)=>{"use strict";n.r(t),n.d(t,{transform:()=>r});n(97606),n(11882),n(27361),n(43393);function r(e,t){let{jsSpec:n}=t;return e}},77793:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>i});var r=n(93527),o=n(34966),s=n(87667);function i(e){return{statePlugins:{err:{reducers:(0,r.default)(e),actions:o,selectors:s}}}}},93527:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>y});var r=n(76986),o=n.n(r),s=n(97606),i=n.n(s),a=n(39022),l=n.n(a),c=n(14418),u=n.n(c),p=n(2250),h=n.n(p),f=n(34966),d=n(43393),m=n(56982);let g={line:0,level:"error",message:"Unknown error"};function y(){return{[f.NEW_THROWN_ERR]:(e,t)=>{let{payload:n}=t,r=o()(g,n,{type:"thrown"});return e.update("errors",(e=>(e||(0,d.List)()).push((0,d.fromJS)(r)))).update("errors",(e=>(0,m.default)(e)))},[f.NEW_THROWN_ERR_BATCH]:(e,t)=>{let{payload:n}=t;return n=i()(n).call(n,(e=>(0,d.fromJS)(o()(g,e,{type:"thrown"})))),e.update("errors",(e=>{var t;return l()(t=e||(0,d.List)()).call(t,(0,d.fromJS)(n))})).update("errors",(e=>(0,m.default)(e)))},[f.NEW_SPEC_ERR]:(e,t)=>{let{payload:n}=t,r=(0,d.fromJS)(n);return r=r.set("type","spec"),e.update("errors",(e=>(e||(0,d.List)()).push((0,d.fromJS)(r)).sortBy((e=>e.get("line"))))).update("errors",(e=>(0,m.default)(e)))},[f.NEW_SPEC_ERR_BATCH]:(e,t)=>{let{payload:n}=t;return n=i()(n).call(n,(e=>(0,d.fromJS)(o()(g,e,{type:"spec"})))),e.update("errors",(e=>{var t;return l()(t=e||(0,d.List)()).call(t,(0,d.fromJS)(n))})).update("errors",(e=>(0,m.default)(e)))},[f.NEW_AUTH_ERR]:(e,t)=>{let{payload:n}=t,r=(0,d.fromJS)(o()({},n));return r=r.set("type","auth"),e.update("errors",(e=>(e||(0,d.List)()).push((0,d.fromJS)(r)))).update("errors",(e=>(0,m.default)(e)))},[f.CLEAR]:(e,t)=>{var n;let{payload:r}=t;if(!r||!e.get("errors"))return e;let o=u()(n=e.get("errors")).call(n,(e=>{var t;return h()(t=e.keySeq()).call(t,(t=>{const n=e.get(t),o=r[t];return!o||n!==o}))}));return e.merge({errors:o})},[f.CLEAR_BY]:(e,t)=>{var n;let{payload:r}=t;if(!r||"function"!=typeof r)return e;let o=u()(n=e.get("errors")).call(n,(e=>r(e)));return e.merge({errors:o})}}}},87667:(e,t,n)=>{"use strict";n.r(t),n.d(t,{allErrors:()=>s,lastError:()=>i});var r=n(43393),o=n(20573);const s=(0,o.P1)((e=>e),(e=>e.get("errors",(0,r.List)()))),i=(0,o.P1)(s,(e=>e.last()))},49978:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(4309);function o(){return{fn:{opsFilter:r.default}}}},4309:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>a});var r=n(14418),o=n.n(r),s=n(11882),i=n.n(s);function a(e,t){return o()(e).call(e,((e,n)=>-1!==i()(n).call(n,t)))}},47349:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>l});var r=n(67294),o=n(94184),s=n.n(o),i=n(12603);const a=e=>{let{expanded:t,children:n,onChange:o}=e;const a=(0,i.useComponent)("ChevronRightIcon"),l=(0,r.useCallback)((e=>{o(e,!t)}),[t,o]);return r.createElement("button",{type:"button",className:"json-schema-2020-12-accordion",onClick:l},r.createElement("div",{className:"json-schema-2020-12-accordion__children"},n),r.createElement("span",{className:s()("json-schema-2020-12-accordion__icon",{"json-schema-2020-12-accordion__icon--expanded":t,"json-schema-2020-12-accordion__icon--collapsed":!t})},r.createElement(a,null)))};a.defaultProps={expanded:!1};const l=a},36867:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);const o=e=>{let{expanded:t,onClick:n}=e;const o=(0,r.useCallback)((e=>{n(e,!t)}),[t,n]);return r.createElement("button",{type:"button",className:"json-schema-2020-12-expand-deep-button",onClick:o},t?"Collapse all":"Expand all")}},22675:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>p});var r=n(97606),o=n.n(r),s=n(67294),i=n(94184),a=n.n(i),l=(n(16648),n(12603)),c=n(69006);const u=(0,s.forwardRef)(((e,t)=>{let{schema:n,name:r,dependentRequired:i,onExpand:u}=e;const p=(0,l.useFn)(),h=(0,l.useIsExpanded)(),f=(0,l.useIsExpandedDeeply)(),[d,m]=(0,s.useState)(h||f),[g,y]=(0,s.useState)(f),[v,b]=(0,l.useLevel)(),w=(0,l.useIsEmbedded)(),E=p.isExpandable(n)||i.length>0,x=(0,l.useIsCircular)(n),S=(0,l.useRenderedSchemas)(n),_=p.stringifyConstraints(n),j=(0,l.useComponent)("Accordion"),O=(0,l.useComponent)("Keyword$schema"),k=(0,l.useComponent)("Keyword$vocabulary"),A=(0,l.useComponent)("Keyword$id"),C=(0,l.useComponent)("Keyword$anchor"),P=(0,l.useComponent)("Keyword$dynamicAnchor"),N=(0,l.useComponent)("Keyword$ref"),I=(0,l.useComponent)("Keyword$dynamicRef"),T=(0,l.useComponent)("Keyword$defs"),R=(0,l.useComponent)("Keyword$comment"),M=(0,l.useComponent)("KeywordAllOf"),D=(0,l.useComponent)("KeywordAnyOf"),F=(0,l.useComponent)("KeywordOneOf"),L=(0,l.useComponent)("KeywordNot"),B=(0,l.useComponent)("KeywordIf"),$=(0,l.useComponent)("KeywordThen"),q=(0,l.useComponent)("KeywordElse"),U=(0,l.useComponent)("KeywordDependentSchemas"),z=(0,l.useComponent)("KeywordPrefixItems"),V=(0,l.useComponent)("KeywordItems"),W=(0,l.useComponent)("KeywordContains"),J=(0,l.useComponent)("KeywordProperties"),K=(0,l.useComponent)("KeywordPatternProperties"),H=(0,l.useComponent)("KeywordAdditionalProperties"),G=(0,l.useComponent)("KeywordPropertyNames"),Z=(0,l.useComponent)("KeywordUnevaluatedItems"),Y=(0,l.useComponent)("KeywordUnevaluatedProperties"),X=(0,l.useComponent)("KeywordType"),Q=(0,l.useComponent)("KeywordEnum"),ee=(0,l.useComponent)("KeywordConst"),te=(0,l.useComponent)("KeywordConstraint"),ne=(0,l.useComponent)("KeywordDependentRequired"),re=(0,l.useComponent)("KeywordContentSchema"),oe=(0,l.useComponent)("KeywordTitle"),se=(0,l.useComponent)("KeywordDescription"),ie=(0,l.useComponent)("KeywordDefault"),ae=(0,l.useComponent)("KeywordDeprecated"),le=(0,l.useComponent)("KeywordReadOnly"),ce=(0,l.useComponent)("KeywordWriteOnly"),ue=(0,l.useComponent)("ExpandDeepButton");(0,s.useEffect)((()=>{y(f)}),[f]),(0,s.useEffect)((()=>{y(g)}),[g]);const pe=(0,s.useCallback)(((e,t)=>{m(t),!t&&y(!1),u(e,t,!1)}),[u]),he=(0,s.useCallback)(((e,t)=>{m(t),y(t),u(e,t,!0)}),[u]);return s.createElement(c.JSONSchemaLevelContext.Provider,{value:b},s.createElement(c.JSONSchemaDeepExpansionContext.Provider,{value:g},s.createElement(c.JSONSchemaCyclesContext.Provider,{value:S},s.createElement("article",{ref:t,"data-json-schema-level":v,className:a()("json-schema-2020-12",{"json-schema-2020-12--embedded":w,"json-schema-2020-12--circular":x})},s.createElement("div",{className:"json-schema-2020-12-head"},E&&!x?s.createElement(s.Fragment,null,s.createElement(j,{expanded:d,onChange:pe},s.createElement(oe,{title:r,schema:n})),s.createElement(ue,{expanded:d,onClick:he})):s.createElement(oe,{title:r,schema:n}),s.createElement(ae,{schema:n}),s.createElement(le,{schema:n}),s.createElement(ce,{schema:n}),s.createElement(X,{schema:n,isCircular:x}),_.length>0&&o()(_).call(_,(e=>s.createElement(te,{key:`${e.scope}-${e.value}`,constraint:e})))),s.createElement("div",{className:a()("json-schema-2020-12-body",{"json-schema-2020-12-body--collapsed":!d})},d&&s.createElement(s.Fragment,null,s.createElement(se,{schema:n}),!x&&E&&s.createElement(s.Fragment,null,s.createElement(J,{schema:n}),s.createElement(K,{schema:n}),s.createElement(H,{schema:n}),s.createElement(Y,{schema:n}),s.createElement(G,{schema:n}),s.createElement(M,{schema:n}),s.createElement(D,{schema:n}),s.createElement(F,{schema:n}),s.createElement(L,{schema:n}),s.createElement(B,{schema:n}),s.createElement($,{schema:n}),s.createElement(q,{schema:n}),s.createElement(U,{schema:n}),s.createElement(z,{schema:n}),s.createElement(V,{schema:n}),s.createElement(Z,{schema:n}),s.createElement(W,{schema:n}),s.createElement(re,{schema:n})),s.createElement(Q,{schema:n}),s.createElement(ee,{schema:n}),s.createElement(ne,{schema:n,dependentRequired:i}),s.createElement(ie,{schema:n}),s.createElement(O,{schema:n}),s.createElement(k,{schema:n}),s.createElement(A,{schema:n}),s.createElement(C,{schema:n}),s.createElement(P,{schema:n}),s.createElement(N,{schema:n}),!x&&E&&s.createElement(T,{schema:n}),s.createElement(I,{schema:n}),s.createElement(R,{schema:n})))))))}));u.defaultProps={name:"",dependentRequired:[],onExpand:()=>{}};const p=u},12260:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);const o=()=>r.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"24",height:"24",viewBox:"0 0 24 24"},r.createElement("path",{d:"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"}))},64922:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);n(16648);const o=e=>{let{schema:t}=e;return null!=t&&t.$anchor?r.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--$anchor"},r.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary"},"$anchor"),r.createElement("span",{className:"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary"},t.$anchor)):null}},4685:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);n(16648);const o=e=>{let{schema:t}=e;return null!=t&&t.$comment?r.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--$comment"},r.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary"},"$comment"),r.createElement("span",{className:"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary"},t.$comment)):null}},36418:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>d});var r=n(28222),o=n.n(r),s=n(97606),i=n.n(s),a=n(2018),l=n.n(a),c=n(67294),u=n(94184),p=n.n(u),h=(n(16648),n(12603)),f=n(69006);const d=e=>{var t;let{schema:n}=e;const r=(null==n?void 0:n.$defs)||{},s=(0,h.useIsExpandedDeeply)(),[a,u]=(0,c.useState)(s),[d,m]=(0,c.useState)(!1),g=(0,h.useComponent)("Accordion"),y=(0,h.useComponent)("ExpandDeepButton"),v=(0,h.useComponent)("JSONSchema"),b=(0,c.useCallback)((()=>{u((e=>!e))}),[]),w=(0,c.useCallback)(((e,t)=>{u(t),m(t)}),[]);return 0===o()(r).length?null:c.createElement(f.JSONSchemaDeepExpansionContext.Provider,{value:d},c.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--$defs"},c.createElement(g,{expanded:a,onChange:b},c.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary"},"$defs")),c.createElement(y,{expanded:a,onClick:w}),c.createElement("strong",{className:"json-schema-2020-12__attribute json-schema-2020-12__attribute--primary"},"object"),c.createElement("ul",{className:p()("json-schema-2020-12-keyword__children",{"json-schema-2020-12-keyword__children--collapsed":!a})},a&&c.createElement(c.Fragment,null,i()(t=l()(r)).call(t,(e=>{let[t,n]=e;return c.createElement("li",{key:t,className:"json-schema-2020-12-property"},c.createElement(v,{name:t,schema:n}))}))))))}},51338:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);n(16648);const o=e=>{let{schema:t}=e;return null!=t&&t.$dynamicAnchor?r.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--$dynamicAnchor"},r.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary"},"$dynamicAnchor"),r.createElement("span",{className:"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary"},t.$dynamicAnchor)):null}},27655:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);n(16648);const o=e=>{let{schema:t}=e;return null!=t&&t.$dynamicRef?r.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--$dynamicRef"},r.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary"},"$dynamicRef"),r.createElement("span",{className:"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary"},t.$dynamicRef)):null}},93460:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);n(16648);const o=e=>{let{schema:t}=e;return null!=t&&t.$id?r.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--$id"},r.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary"},"$id"),r.createElement("span",{className:"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary"},t.$id)):null}},72348:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);n(16648);const o=e=>{let{schema:t}=e;return null!=t&&t.$ref?r.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--$ref"},r.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary"},"$ref"),r.createElement("span",{className:"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary"},t.$ref)):null}},69359:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);n(16648);const o=e=>{let{schema:t}=e;return null!=t&&t.$schema?r.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--$schema"},r.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary"},"$schema"),r.createElement("span",{className:"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary"},t.$schema)):null}},7568:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>p});var r=n(97606),o=n.n(r),s=n(2018),i=n.n(s),a=n(67294),l=n(94184),c=n.n(l),u=(n(16648),n(12603));const p=e=>{var t;let{schema:n}=e;const r=(0,u.useIsExpandedDeeply)(),[s,l]=(0,a.useState)(r),p=(0,u.useComponent)("Accordion"),h=(0,a.useCallback)((()=>{l((e=>!e))}),[]);return null!=n&&n.$vocabulary?"object"!=typeof n.$vocabulary?null:a.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--$vocabulary"},a.createElement(p,{expanded:s,onChange:h},a.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary"},"$vocabulary")),a.createElement("strong",{className:"json-schema-2020-12__attribute json-schema-2020-12__attribute--primary"},"object"),a.createElement("ul",null,s&&o()(t=i()(n.$vocabulary)).call(t,(e=>{let[t,n]=e;return a.createElement("li",{key:t,className:c()("json-schema-2020-12-$vocabulary-uri",{"json-schema-2020-12-$vocabulary-uri--disabled":!n})},a.createElement("span",{className:"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary"},t))})))):null}},65253:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});var r=n(67294),o=(n(16648),n(12603));const s=e=>{let{schema:t}=e;const n=(0,o.useFn)(),{additionalProperties:s}=t,i=(0,o.useComponent)("JSONSchema");if(!n.hasKeyword(t,"additionalProperties"))return null;const a=r.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary"},"Additional properties");return r.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--additionalProperties"},!0===s?r.createElement(r.Fragment,null,a,r.createElement("span",{className:"json-schema-2020-12__attribute json-schema-2020-12__attribute--primary"},"allowed")):!1===s?r.createElement(r.Fragment,null,a,r.createElement("span",{className:"json-schema-2020-12__attribute json-schema-2020-12__attribute--primary"},"forbidden")):r.createElement(i,{name:a,schema:s}))}},46457:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>h});var r=n(58309),o=n.n(r),s=n(97606),i=n.n(s),a=n(67294),l=n(94184),c=n.n(l),u=(n(16648),n(12603)),p=n(69006);const h=e=>{let{schema:t}=e;const n=(null==t?void 0:t.allOf)||[],r=(0,u.useFn)(),s=(0,u.useIsExpandedDeeply)(),[l,h]=(0,a.useState)(s),[f,d]=(0,a.useState)(!1),m=(0,u.useComponent)("Accordion"),g=(0,u.useComponent)("ExpandDeepButton"),y=(0,u.useComponent)("JSONSchema"),v=(0,u.useComponent)("KeywordType"),b=(0,a.useCallback)((()=>{h((e=>!e))}),[]),w=(0,a.useCallback)(((e,t)=>{h(t),d(t)}),[]);return o()(n)&&0!==n.length?a.createElement(p.JSONSchemaDeepExpansionContext.Provider,{value:f},a.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--allOf"},a.createElement(m,{expanded:l,onChange:b},a.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary"},"All of")),a.createElement(g,{expanded:l,onClick:w}),a.createElement(v,{schema:{allOf:n}}),a.createElement("ul",{className:c()("json-schema-2020-12-keyword__children",{"json-schema-2020-12-keyword__children--collapsed":!l})},l&&a.createElement(a.Fragment,null,i()(n).call(n,((e,t)=>a.createElement("li",{key:`#${t}`,className:"json-schema-2020-12-property"},a.createElement(y,{name:`#${t} ${r.getTitle(e)}`,schema:e})))))))):null}},8776:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>h});var r=n(58309),o=n.n(r),s=n(97606),i=n.n(s),a=n(67294),l=n(94184),c=n.n(l),u=(n(16648),n(12603)),p=n(69006);const h=e=>{let{schema:t}=e;const n=(null==t?void 0:t.anyOf)||[],r=(0,u.useFn)(),s=(0,u.useIsExpandedDeeply)(),[l,h]=(0,a.useState)(s),[f,d]=(0,a.useState)(!1),m=(0,u.useComponent)("Accordion"),g=(0,u.useComponent)("ExpandDeepButton"),y=(0,u.useComponent)("JSONSchema"),v=(0,u.useComponent)("KeywordType"),b=(0,a.useCallback)((()=>{h((e=>!e))}),[]),w=(0,a.useCallback)(((e,t)=>{h(t),d(t)}),[]);return o()(n)&&0!==n.length?a.createElement(p.JSONSchemaDeepExpansionContext.Provider,{value:f},a.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--anyOf"},a.createElement(m,{expanded:l,onChange:b},a.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary"},"Any of")),a.createElement(g,{expanded:l,onClick:w}),a.createElement(v,{schema:{anyOf:n}}),a.createElement("ul",{className:c()("json-schema-2020-12-keyword__children",{"json-schema-2020-12-keyword__children--collapsed":!l})},l&&a.createElement(a.Fragment,null,i()(n).call(n,((e,t)=>a.createElement("li",{key:`#${t}`,className:"json-schema-2020-12-property"},a.createElement(y,{name:`#${t} ${r.getTitle(e)}`,schema:e})))))))):null}},27308:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});var r=n(67294),o=(n(16648),n(12603));const s=e=>{let{schema:t}=e;const n=(0,o.useFn)();return n.hasKeyword(t,"const")?r.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--const"},r.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary"},"Const"),r.createElement("span",{className:"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--const"},n.stringify(t.const))):null}},69956:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});var r=n(67294);const o=e=>{let{constraint:t}=e;return r.createElement("span",{className:`json-schema-2020-12__constraint json-schema-2020-12__constraint--${t.scope}`},t.value)},s=r.memo(o)},38993:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});var r=n(67294),o=(n(16648),n(12603));const s=e=>{let{schema:t}=e;const n=(0,o.useFn)(),s=(0,o.useComponent)("JSONSchema");if(!n.hasKeyword(t,"contains"))return null;const i=r.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary"},"Contains");return r.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--contains"},r.createElement(s,{name:i,schema:t.contains}))}},3484:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});var r=n(67294),o=(n(16648),n(12603));const s=e=>{let{schema:t}=e;const n=(0,o.useFn)(),s=(0,o.useComponent)("JSONSchema");if(!n.hasKeyword(t,"contentSchema"))return null;const i=r.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary"},"Content schema");return r.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--contentSchema"},r.createElement(s,{name:i,schema:t.contentSchema}))}},55148:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});var r=n(67294),o=(n(16648),n(12603));const s=e=>{let{schema:t}=e;const n=(0,o.useFn)();return n.hasKeyword(t,"default")?r.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--default"},r.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary"},"Default"),r.createElement("span",{className:"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--const"},n.stringify(t.default))):null}},24539:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>i});var r=n(97606),o=n.n(r),s=n(67294);n(16648);const i=e=>{let{dependentRequired:t}=e;return 0===t.length?null:s.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--dependentRequired"},s.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary"},"Required when defined"),s.createElement("ul",null,o()(t).call(t,(e=>s.createElement("li",{key:e},s.createElement("span",{className:"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--warning"},e))))))}},26076:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>d});var r=n(28222),o=n.n(r),s=n(97606),i=n.n(s),a=n(2018),l=n.n(a),c=n(67294),u=n(94184),p=n.n(u),h=(n(16648),n(12603)),f=n(69006);const d=e=>{var t;let{schema:n}=e;const r=(null==n?void 0:n.dependentSchemas)||[],s=(0,h.useIsExpandedDeeply)(),[a,u]=(0,c.useState)(s),[d,m]=(0,c.useState)(!1),g=(0,h.useComponent)("Accordion"),y=(0,h.useComponent)("ExpandDeepButton"),v=(0,h.useComponent)("JSONSchema"),b=(0,c.useCallback)((()=>{u((e=>!e))}),[]),w=(0,c.useCallback)(((e,t)=>{u(t),m(t)}),[]);return"object"!=typeof r||0===o()(r).length?null:c.createElement(f.JSONSchemaDeepExpansionContext.Provider,{value:d},c.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--dependentSchemas"},c.createElement(g,{expanded:a,onChange:b},c.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary"},"Dependent schemas")),c.createElement(y,{expanded:a,onClick:w}),c.createElement("strong",{className:"json-schema-2020-12__attribute json-schema-2020-12__attribute--primary"},"object"),c.createElement("ul",{className:p()("json-schema-2020-12-keyword__children",{"json-schema-2020-12-keyword__children--collapsed":!a})},a&&c.createElement(c.Fragment,null,i()(t=l()(r)).call(t,(e=>{let[t,n]=e;return c.createElement("li",{key:t,className:"json-schema-2020-12-property"},c.createElement(v,{name:t,schema:n}))}))))))}},26661:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);n(16648);const o=e=>{let{schema:t}=e;return!0!==(null==t?void 0:t.deprecated)?null:r.createElement("span",{className:"json-schema-2020-12__attribute json-schema-2020-12__attribute--warning"},"deprecated")}},79446:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);n(16648);const o=e=>{let{schema:t}=e;return null!=t&&t.description?r.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--description"},r.createElement("div",{className:"json-schema-2020-12-core-keyword__value json-schema-2020-12-core-keyword__value--secondary"},t.description)):null}},67207:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});var r=n(67294),o=(n(16648),n(12603));const s=e=>{let{schema:t}=e;const n=(0,o.useFn)(),s=(0,o.useComponent)("JSONSchema");if(!n.hasKeyword(t,"else"))return null;const i=r.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary"},"Else");return r.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--if"},r.createElement(s,{name:i,schema:t.else}))}},91805:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>c});var r=n(58309),o=n.n(r),s=n(97606),i=n.n(s),a=n(67294),l=(n(16648),n(12603));const c=e=>{var t;let{schema:n}=e;const r=(0,l.useFn)();return o()(null==n?void 0:n.enum)?a.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--enum"},a.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary"},"Allowed values"),a.createElement("ul",null,i()(t=n.enum).call(t,(e=>{const t=r.stringify(e);return a.createElement("li",{key:t},a.createElement("span",{className:"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--const"},t))})))):null}},40487:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});var r=n(67294),o=(n(16648),n(12603));const s=e=>{let{schema:t}=e;const n=(0,o.useFn)(),s=(0,o.useComponent)("JSONSchema");if(!n.hasKeyword(t,"if"))return null;const i=r.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary"},"If");return r.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--if"},r.createElement(s,{name:i,schema:t.if}))}},89206:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});var r=n(67294),o=(n(16648),n(12603));const s=e=>{let{schema:t}=e;const n=(0,o.useFn)(),s=(0,o.useComponent)("JSONSchema");if(!n.hasKeyword(t,"items"))return null;const i=r.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary"},"Items");return r.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--items"},r.createElement(s,{name:i,schema:t.items}))}},65174:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});var r=n(67294),o=(n(16648),n(12603));const s=e=>{let{schema:t}=e;const n=(0,o.useFn)(),s=(0,o.useComponent)("JSONSchema");if(!n.hasKeyword(t,"not"))return null;const i=r.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary"},"Not");return r.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--not"},r.createElement(s,{name:i,schema:t.not}))}},13834:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>h});var r=n(58309),o=n.n(r),s=n(97606),i=n.n(s),a=n(67294),l=n(94184),c=n.n(l),u=(n(16648),n(12603)),p=n(69006);const h=e=>{let{schema:t}=e;const n=(null==t?void 0:t.oneOf)||[],r=(0,u.useFn)(),s=(0,u.useIsExpandedDeeply)(),[l,h]=(0,a.useState)(s),[f,d]=(0,a.useState)(!1),m=(0,u.useComponent)("Accordion"),g=(0,u.useComponent)("ExpandDeepButton"),y=(0,u.useComponent)("JSONSchema"),v=(0,u.useComponent)("KeywordType"),b=(0,a.useCallback)((()=>{h((e=>!e))}),[]),w=(0,a.useCallback)(((e,t)=>{h(t),d(t)}),[]);return o()(n)&&0!==n.length?a.createElement(p.JSONSchemaDeepExpansionContext.Provider,{value:f},a.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--oneOf"},a.createElement(m,{expanded:l,onChange:b},a.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary"},"One of")),a.createElement(g,{expanded:l,onClick:w}),a.createElement(v,{schema:{oneOf:n}}),a.createElement("ul",{className:c()("json-schema-2020-12-keyword__children",{"json-schema-2020-12-keyword__children--collapsed":!l})},l&&a.createElement(a.Fragment,null,i()(n).call(n,((e,t)=>a.createElement("li",{key:`#${t}`,className:"json-schema-2020-12-property"},a.createElement(y,{name:`#${t} ${r.getTitle(e)}`,schema:e})))))))):null}},36746:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>p});var r=n(28222),o=n.n(r),s=n(97606),i=n.n(s),a=n(2018),l=n.n(a),c=n(67294),u=(n(16648),n(12603));const p=e=>{var t;let{schema:n}=e;const r=(null==n?void 0:n.patternProperties)||{},s=(0,u.useComponent)("JSONSchema");return 0===o()(r).length?null:c.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--patternProperties"},c.createElement("ul",null,i()(t=l()(r)).call(t,(e=>{let[t,n]=e;return c.createElement("li",{key:t,className:"json-schema-2020-12-property"},c.createElement(s,{name:t,schema:n}))}))))}},93971:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>h});var r=n(58309),o=n.n(r),s=n(97606),i=n.n(s),a=n(67294),l=n(94184),c=n.n(l),u=(n(16648),n(12603)),p=n(69006);const h=e=>{let{schema:t}=e;const n=(null==t?void 0:t.prefixItems)||[],r=(0,u.useFn)(),s=(0,u.useIsExpandedDeeply)(),[l,h]=(0,a.useState)(s),[f,d]=(0,a.useState)(!1),m=(0,u.useComponent)("Accordion"),g=(0,u.useComponent)("ExpandDeepButton"),y=(0,u.useComponent)("JSONSchema"),v=(0,u.useComponent)("KeywordType"),b=(0,a.useCallback)((()=>{h((e=>!e))}),[]),w=(0,a.useCallback)(((e,t)=>{h(t),d(t)}),[]);return o()(n)&&0!==n.length?a.createElement(p.JSONSchemaDeepExpansionContext.Provider,{value:f},a.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--prefixItems"},a.createElement(m,{expanded:l,onChange:b},a.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary"},"Prefix items")),a.createElement(g,{expanded:l,onClick:w}),a.createElement(v,{schema:{prefixItems:n}}),a.createElement("ul",{className:c()("json-schema-2020-12-keyword__children",{"json-schema-2020-12-keyword__children--collapsed":!l})},l&&a.createElement(a.Fragment,null,i()(n).call(n,((e,t)=>a.createElement("li",{key:`#${t}`,className:"json-schema-2020-12-property"},a.createElement(y,{name:`#${t} ${r.getTitle(e)}`,schema:e})))))))):null}},25472:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>y});var r=n(58309),o=n.n(r),s=n(28222),i=n.n(s),a=n(97606),l=n.n(a),c=n(2018),u=n.n(c),p=n(58118),h=n.n(p),f=n(67294),d=n(94184),m=n.n(d),g=(n(16648),n(12603));const y=e=>{var t;let{schema:n}=e;const r=(0,g.useFn)(),s=(null==n?void 0:n.properties)||{},a=o()(null==n?void 0:n.required)?n.required:[],c=(0,g.useComponent)("JSONSchema");return 0===i()(s).length?null:f.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--properties"},f.createElement("ul",null,l()(t=u()(s)).call(t,(e=>{let[t,o]=e;const s=h()(a).call(a,t),i=r.getDependentRequired(t,n);return f.createElement("li",{key:t,className:m()("json-schema-2020-12-property",{"json-schema-2020-12-property--required":s})},f.createElement(c,{name:t,schema:o,dependentRequired:i}))}))))}},42338:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});var r=n(67294),o=(n(16648),n(12603));const s=e=>{let{schema:t}=e;const n=(0,o.useFn)(),{propertyNames:s}=t,i=(0,o.useComponent)("JSONSchema"),a=r.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary"},"Property names");return n.hasKeyword(t,"propertyNames")?r.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--propertyNames"},r.createElement(i,{name:a,schema:s})):null}},16456:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);n(16648);const o=e=>{let{schema:t}=e;return!0!==(null==t?void 0:t.readOnly)?null:r.createElement("span",{className:"json-schema-2020-12__attribute json-schema-2020-12__attribute--muted"},"read-only")}},67401:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});var r=n(67294),o=(n(16648),n(12603));const s=e=>{let{schema:t}=e;const n=(0,o.useFn)(),s=(0,o.useComponent)("JSONSchema");if(!n.hasKeyword(t,"then"))return null;const i=r.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary"},"Then");return r.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--then"},r.createElement(s,{name:i,schema:t.then}))}},78137:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>i});var r=n(67294),o=(n(16648),n(12603));const s=e=>{let{title:t,schema:n}=e;const s=(0,o.useFn)();return t||s.getTitle(n)?r.createElement("div",{className:"json-schema-2020-12__title"},t||s.getTitle(n)):null};s.defaultProps={title:""};const i=s},22285:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>i});var r=n(67294),o=(n(16648),n(12603));const s=e=>{let{schema:t,isCircular:n}=e;const s=(0,o.useFn)().getType(t),i=n?" [circular]":"";return r.createElement("strong",{className:"json-schema-2020-12__attribute json-schema-2020-12__attribute--primary"},`${s}${i}`)};s.defaultProps={isCircular:!1};const i=s},85828:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});var r=n(67294),o=(n(16648),n(12603));const s=e=>{let{schema:t}=e;const n=(0,o.useFn)(),{unevaluatedItems:s}=t,i=(0,o.useComponent)("JSONSchema");if(!n.hasKeyword(t,"unevaluatedItems"))return null;const a=r.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary"},"Unevaluated items");return r.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--unevaluatedItems"},r.createElement(i,{name:a,schema:s}))}},6907:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});var r=n(67294),o=(n(16648),n(12603));const s=e=>{let{schema:t}=e;const n=(0,o.useFn)(),{unevaluatedProperties:s}=t,i=(0,o.useComponent)("JSONSchema");if(!n.hasKeyword(t,"unevaluatedProperties"))return null;const a=r.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary"},"Unevaluated properties");return r.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--unevaluatedProperties"},r.createElement(i,{name:a,schema:s}))}},15789:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);n(16648);const o=e=>{let{schema:t}=e;return!0!==(null==t?void 0:t.writeOnly)?null:r.createElement("span",{className:"json-schema-2020-12__attribute json-schema-2020-12__attribute--muted"},"write-only")}},69006:(e,t,n)=>{"use strict";n.r(t),n.d(t,{JSONSchemaContext:()=>i,JSONSchemaCyclesContext:()=>c,JSONSchemaDeepExpansionContext:()=>l,JSONSchemaLevelContext:()=>a});var r=n(82737),o=n.n(r),s=n(67294);const i=(0,s.createContext)(null);i.displayName="JSONSchemaContext";const a=(0,s.createContext)(0);a.displayName="JSONSchemaLevelContext";const l=(0,s.createContext)(!1);l.displayName="JSONSchemaDeepExpansionContext";const c=(0,s.createContext)(new(o()))},33499:(e,t,n)=>{"use strict";n.r(t),n.d(t,{getDependentRequired:()=>F,getTitle:()=>C,getType:()=>P,hasKeyword:()=>I,isBooleanJSONSchema:()=>N,isExpandable:()=>T,stringify:()=>R,stringifyConstraints:()=>D,upperFirst:()=>A});var r=n(24278),o=n.n(r),s=n(19030),i=n.n(s),a=n(58309),l=n.n(a),c=n(97606),u=n.n(c),p=n(58118),h=n.n(p),f=n(91086),d=n.n(f),m=n(14418),g=n.n(m),y=n(35627),v=n.n(y),b=n(25110),w=n.n(b),E=n(24282),x=n.n(E),S=n(2018),_=n.n(S),j=n(82737),O=n.n(j),k=n(12603);const A=e=>"string"==typeof e?`${e.charAt(0).toUpperCase()}${o()(e).call(e,1)}`:e,C=e=>{const t=(0,k.useFn)();return null!=e&&e.title?t.upperFirst(e.title):null!=e&&e.$anchor?t.upperFirst(e.$anchor):null!=e&&e.$id?e.$id:""},P=function(e){var t,n;let r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:new(i());const o=(0,k.useFn)();if(null==e)return"any";if(o.isBooleanJSONSchema(e))return e?"any":"never";if("object"!=typeof e)return"any";if(r.has(e))return"any";r.add(e);const{type:s,prefixItems:a,items:c}=e,p=()=>{if(l()(a)){const e=u()(a).call(a,(e=>P(e,r))),t=c?P(c,r):"any";return`array<[${e.join(", ")}], ${t}>`}if(c){return`array<${P(c,r)}>`}return"array"};if(e.not&&"any"===P(e.not))return"never";const f=l()(s)?u()(s).call(s,(e=>"array"===e?p():e)).join(" | "):"array"===s?p():h()(t=["null","boolean","object","array","number","string"]).call(t,s)?s:(()=>{var t,n;if(Object.hasOwn(e,"prefixItems")||Object.hasOwn(e,"items")||Object.hasOwn(e,"contains"))return p();if(Object.hasOwn(e,"properties")||Object.hasOwn(e,"additionalProperties")||Object.hasOwn(e,"patternProperties"))return"object";if(h()(t=["int32","int64"]).call(t,e.format))return"integer";if(h()(n=["float","double"]).call(n,e.format))return"number";if(Object.hasOwn(e,"minimum")||Object.hasOwn(e,"maximum")||Object.hasOwn(e,"exclusiveMinimum")||Object.hasOwn(e,"exclusiveMaximum")||Object.hasOwn(e,"multipleOf"))return"number | integer";if(Object.hasOwn(e,"pattern")||Object.hasOwn(e,"format")||Object.hasOwn(e,"minLength")||Object.hasOwn(e,"maxLength"))return"string";if(void 0!==e.const){if(null===e.const)return"null";if("boolean"==typeof e.const)return"boolean";if("number"==typeof e.const)return d()(e.const)?"integer":"number";if("string"==typeof e.const)return"string";if(l()(e.const))return"array";if("object"==typeof e.const)return"object"}return null})(),m=(t,n)=>{if(l()(e[t])){var o;return`(${u()(o=e[t]).call(o,(e=>P(e,r))).join(n)})`}return null},y=m("oneOf"," | "),v=m("anyOf"," | "),b=m("allOf"," & "),w=g()(n=[f,y,v,b]).call(n,Boolean).join(" | ");return r.delete(e),w||"any"},N=e=>"boolean"==typeof e,I=(e,t)=>null!==e&&"object"==typeof e&&Object.hasOwn(e,t),T=e=>{const t=(0,k.useFn)();return(null==e?void 0:e.$schema)||(null==e?void 0:e.$vocabulary)||(null==e?void 0:e.$id)||(null==e?void 0:e.$anchor)||(null==e?void 0:e.$dynamicAnchor)||(null==e?void 0:e.$ref)||(null==e?void 0:e.$dynamicRef)||(null==e?void 0:e.$defs)||(null==e?void 0:e.$comment)||(null==e?void 0:e.allOf)||(null==e?void 0:e.anyOf)||(null==e?void 0:e.oneOf)||t.hasKeyword(e,"not")||t.hasKeyword(e,"if")||t.hasKeyword(e,"then")||t.hasKeyword(e,"else")||(null==e?void 0:e.dependentSchemas)||(null==e?void 0:e.prefixItems)||t.hasKeyword(e,"items")||t.hasKeyword(e,"contains")||(null==e?void 0:e.properties)||(null==e?void 0:e.patternProperties)||t.hasKeyword(e,"additionalProperties")||t.hasKeyword(e,"propertyNames")||t.hasKeyword(e,"unevaluatedItems")||t.hasKeyword(e,"unevaluatedProperties")||(null==e?void 0:e.description)||(null==e?void 0:e.enum)||t.hasKeyword(e,"const")||t.hasKeyword(e,"contentSchema")||t.hasKeyword(e,"default")},R=e=>{var t;return null===e||h()(t=["number","bigint","boolean"]).call(t,typeof e)?String(e):l()(e)?`[${u()(e).call(e,R).join(", ")}]`:v()(e)},M=(e,t,n)=>{const r="number"==typeof t,o="number"==typeof n;return r&&o?t===n?`${t} ${e}`:`[${t}, ${n}] ${e}`:r?`>= ${t} ${e}`:o?`<= ${n} ${e}`:null},D=e=>{const t=[],n=(e=>{if("number"!=typeof(null==e?void 0:e.multipleOf))return null;if(e.multipleOf<=0)return null;if(1===e.multipleOf)return null;const{multipleOf:t}=e;if(d()(t))return`multiple of ${t}`;const n=10**t.toString().split(".")[1].length;return`multiple of ${t*n}/${n}`})(e);null!==n&&t.push({scope:"number",value:n});const r=(e=>{const t=null==e?void 0:e.minimum,n=null==e?void 0:e.maximum,r=null==e?void 0:e.exclusiveMinimum,o=null==e?void 0:e.exclusiveMaximum,s="number"==typeof t,i="number"==typeof n,a="number"==typeof r&&to;if(s&&i)return`${a?"(":"["}${a?r:t}, ${l?o:n}${l?")":"]"}`;if(s)return`${a?">":"≥"} ${a?r:t}`;if(i)return`${l?"<":"≤"} ${l?o:n}`;return null})(e);null!==r&&t.push({scope:"number",value:r}),null!=e&&e.format&&t.push({scope:"string",value:e.format});const o=M("characters",null==e?void 0:e.minLength,null==e?void 0:e.maxLength);null!==o&&t.push({scope:"string",value:o}),null!=e&&e.pattern&&t.push({scope:"string",value:`matches ${null==e?void 0:e.pattern}`}),null!=e&&e.contentMediaType&&t.push({scope:"string",value:`media type: ${e.contentMediaType}`}),null!=e&&e.contentEncoding&&t.push({scope:"string",value:`encoding: ${e.contentEncoding}`});const s=M(null!=e&&e.hasUniqueItems?"unique items":"items",null==e?void 0:e.minItems,null==e?void 0:e.maxItems);null!==s&&t.push({scope:"array",value:s});const i=M("contained items",null==e?void 0:e.minContains,null==e?void 0:e.maxContains);null!==i&&t.push({scope:"array",value:i});const a=M("properties",null==e?void 0:e.minProperties,null==e?void 0:e.maxProperties);return null!==a&&t.push({scope:"object",value:a}),t},F=(e,t)=>{var n;return null!=t&&t.dependentRequired?w()(x()(n=_()(t.dependentRequired)).call(n,((t,n)=>{let[r,o]=n;return l()(o)&&h()(o).call(o,e)?(t.add(r),t):t}),new(O()))):[]}},65077:(e,t,n)=>{"use strict";n.r(t),n.d(t,{withJSONSchemaContext:()=>H});var r=n(67294),o=n(22675),s=n(69359),i=n(7568),a=n(93460),l=n(64922),c=n(51338),u=n(72348),p=n(27655),h=n(36418),f=n(4685),d=n(46457),m=n(8776),g=n(13834),y=n(65174),v=n(40487),b=n(67401),w=n(67207),E=n(26076),x=n(93971),S=n(89206),_=n(38993),j=n(25472),O=n(36746),k=n(65253),A=n(42338),C=n(85828),P=n(6907),N=n(22285),I=n(91805),T=n(27308),R=n(69956),M=n(24539),D=n(3484),F=n(78137),L=n(79446),B=n(55148),$=n(26661),q=n(16456),U=n(15789),z=n(47349),V=n(36867),W=n(12260),J=n(69006),K=n(33499);const H=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const n={components:{JSONSchema:o.default,Keyword$schema:s.default,Keyword$vocabulary:i.default,Keyword$id:a.default,Keyword$anchor:l.default,Keyword$dynamicAnchor:c.default,Keyword$ref:u.default,Keyword$dynamicRef:p.default,Keyword$defs:h.default,Keyword$comment:f.default,KeywordAllOf:d.default,KeywordAnyOf:m.default,KeywordOneOf:g.default,KeywordNot:y.default,KeywordIf:v.default,KeywordThen:b.default,KeywordElse:w.default,KeywordDependentSchemas:E.default,KeywordPrefixItems:x.default,KeywordItems:S.default,KeywordContains:_.default,KeywordProperties:j.default,KeywordPatternProperties:O.default,KeywordAdditionalProperties:k.default,KeywordPropertyNames:A.default,KeywordUnevaluatedItems:C.default,KeywordUnevaluatedProperties:P.default,KeywordType:N.default,KeywordEnum:I.default,KeywordConst:T.default,KeywordConstraint:R.default,KeywordDependentRequired:M.default,KeywordContentSchema:D.default,KeywordTitle:F.default,KeywordDescription:L.default,KeywordDefault:B.default,KeywordDeprecated:$.default,KeywordReadOnly:q.default,KeywordWriteOnly:U.default,Accordion:z.default,ExpandDeepButton:V.default,ChevronRightIcon:W.default,...t.components},config:{default$schema:"https://json-schema.org/draft/2020-12/schema",defaultExpandedLevels:0,...t.config},fn:{upperFirst:K.upperFirst,getTitle:K.getTitle,getType:K.getType,isBooleanJSONSchema:K.isBooleanJSONSchema,hasKeyword:K.hasKeyword,isExpandable:K.isExpandable,stringify:K.stringify,stringifyConstraints:K.stringifyConstraints,getDependentRequired:K.getDependentRequired,...t.fn}},H=t=>r.createElement(J.JSONSchemaContext.Provider,{value:n},r.createElement(e,t));return H.contexts={JSONSchemaContext:J.JSONSchemaContext},H.displayName=e.displayName,H}},12603:(e,t,n)=>{"use strict";n.r(t),n.d(t,{useComponent:()=>l,useConfig:()=>a,useFn:()=>c,useIsCircular:()=>m,useIsEmbedded:()=>p,useIsExpanded:()=>h,useIsExpandedDeeply:()=>f,useLevel:()=>u,useRenderedSchemas:()=>d});var r=n(82737),o=n.n(r),s=n(67294),i=n(69006);const a=()=>{const{config:e}=(0,s.useContext)(i.JSONSchemaContext);return e},l=e=>{const{components:t}=(0,s.useContext)(i.JSONSchemaContext);return t[e]||null},c=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:void 0;const{fn:t}=(0,s.useContext)(i.JSONSchemaContext);return void 0!==e?t[e]:t},u=()=>{const e=(0,s.useContext)(i.JSONSchemaLevelContext);return[e,e+1]},p=()=>{const[e]=u();return e>0},h=()=>{const[e]=u(),{defaultExpandedLevels:t}=a();return t-e>0},f=()=>(0,s.useContext)(i.JSONSchemaDeepExpansionContext),d=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:void 0;if(void 0===e)return(0,s.useContext)(i.JSONSchemaCyclesContext);const t=(0,s.useContext)(i.JSONSchemaCyclesContext);return new(o())([...t,e])},m=e=>d().has(e)},97139:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>Z});var r=n(22675),o=n(69359),s=n(7568),i=n(93460),a=n(64922),l=n(51338),c=n(72348),u=n(27655),p=n(36418),h=n(4685),f=n(46457),d=n(8776),m=n(13834),g=n(65174),y=n(40487),v=n(67401),b=n(67207),w=n(26076),E=n(93971),x=n(89206),S=n(38993),_=n(25472),j=n(36746),O=n(65253),k=n(42338),A=n(85828),C=n(6907),P=n(22285),N=n(91805),I=n(27308),T=n(69956),R=n(24539),M=n(3484),D=n(78137),F=n(79446),L=n(55148),B=n(26661),$=n(16456),q=n(15789),U=n(47349),z=n(36867),V=n(12260),W=n(33499),J=n(78591),K=n(69006),H=n(12603),G=n(65077);const Z=()=>({components:{JSONSchema202012:r.default,JSONSchema202012Keyword$schema:o.default,JSONSchema202012Keyword$vocabulary:s.default,JSONSchema202012Keyword$id:i.default,JSONSchema202012Keyword$anchor:a.default,JSONSchema202012Keyword$dynamicAnchor:l.default,JSONSchema202012Keyword$ref:c.default,JSONSchema202012Keyword$dynamicRef:u.default,JSONSchema202012Keyword$defs:p.default,JSONSchema202012Keyword$comment:h.default,JSONSchema202012KeywordAllOf:f.default,JSONSchema202012KeywordAnyOf:d.default,JSONSchema202012KeywordOneOf:m.default,JSONSchema202012KeywordNot:g.default,JSONSchema202012KeywordIf:y.default,JSONSchema202012KeywordThen:v.default,JSONSchema202012KeywordElse:b.default,JSONSchema202012KeywordDependentSchemas:w.default,JSONSchema202012KeywordPrefixItems:E.default,JSONSchema202012KeywordItems:x.default,JSONSchema202012KeywordContains:S.default,JSONSchema202012KeywordProperties:_.default,JSONSchema202012KeywordPatternProperties:j.default,JSONSchema202012KeywordAdditionalProperties:O.default,JSONSchema202012KeywordPropertyNames:k.default,JSONSchema202012KeywordUnevaluatedItems:A.default,JSONSchema202012KeywordUnevaluatedProperties:C.default,JSONSchema202012KeywordType:P.default,JSONSchema202012KeywordEnum:N.default,JSONSchema202012KeywordConst:I.default,JSONSchema202012KeywordConstraint:T.default,JSONSchema202012KeywordDependentRequired:R.default,JSONSchema202012KeywordContentSchema:M.default,JSONSchema202012KeywordTitle:D.default,JSONSchema202012KeywordDescription:F.default,JSONSchema202012KeywordDefault:L.default,JSONSchema202012KeywordDeprecated:B.default,JSONSchema202012KeywordReadOnly:$.default,JSONSchema202012KeywordWriteOnly:q.default,JSONSchema202012Accordion:U.default,JSONSchema202012ExpandDeepButton:z.default,JSONSchema202012ChevronRightIcon:V.default,withJSONSchema202012Context:G.withJSONSchemaContext,JSONSchema202012DeepExpansionContext:()=>K.JSONSchemaDeepExpansionContext},fn:{upperFirst:W.upperFirst,jsonSchema202012:{isExpandable:W.isExpandable,hasKeyword:W.hasKeyword,useFn:H.useFn,useConfig:H.useConfig,useComponent:H.useComponent,useIsExpandedDeeply:H.useIsExpandedDeeply,sampleFromSchema:J.sampleFromSchema,sampleFromSchemaGeneric:J.sampleFromSchemaGeneric,sampleEncoderAPI:J.encoderAPI,sampleFormatAPI:J.formatAPI,sampleMediaTypeAPI:J.mediaTypeAPI,createXMLExample:J.createXMLExample,memoizedSampleFromSchema:J.memoizedSampleFromSchema,memoizedCreateXMLExample:J.memoizedCreateXMLExample}}})},16648:(e,t,n)=>{"use strict";n.r(t),n.d(t,{booleanSchema:()=>i,objectSchema:()=>s,schema:()=>a});var r=n(45697),o=n.n(r);const s=o().object,i=o().bool,a=o().oneOfType([s,i])},9507:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});const r=new(n(70674).default),o=(e,t)=>"function"==typeof t?r.register(e,t):null===t?r.unregister(e):r.get(e);o.getDefaults=()=>r.defaults;const s=o},22906:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});const r=new(n(14215).default),o=(e,t)=>"function"==typeof t?r.register(e,t):null===t?r.unregister(e):r.get(e)},90537:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});const r=new(n(43782).default),o=(e,t)=>{if("function"==typeof t)return r.register(e,t);if(null===t)return r.unregister(e);const n=e.split(";").at(0),o=`${n.split("/").at(0)}/*`;return r.get(e)||r.get(n)||r.get(o)};o.getDefaults=()=>r.defaults;const s=o},70674:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>w});var r=n(61125),o=n.n(r),s=n(47667),i=n.n(s),a=n(28886),l=n.n(a),c=n(14215),u=n(41433),p=n(58509),h=n(44366),f=n(65037),d=n(5709),m=n(54180),g=n(91967);function y(e,t,n){!function(e,t){if(t.has(e))throw new TypeError("Cannot initialize the same private elements twice on an object")}(e,t),t.set(e,n)}var v=new(l());class b extends c.default{constructor(){super(...arguments),y(this,v,{writable:!0,value:{"7bit":u.default,"8bit":p.default,binary:h.default,"quoted-printable":f.default,base16:d.default,base32:m.default,base64:g.default}}),o()(this,"data",{...i()(this,v)})}get defaults(){return{...i()(this,v)}}}const w=b},43782:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>v});var r=n(61125),o=n.n(r),s=n(47667),i=n.n(s),a=n(28886),l=n.n(a),c=n(14215),u=n(65378),p=n(46724),h=n(54342),f=n(92974),d=n(2672);function m(e,t,n){!function(e,t){if(t.has(e))throw new TypeError("Cannot initialize the same private elements twice on an object")}(e,t),t.set(e,n)}var g=new(l());class y extends c.default{constructor(){super(...arguments),m(this,g,{writable:!0,value:{...u.default,...p.default,...h.default,...f.default,...d.default}}),o()(this,"data",{...i()(this,g)})}get defaults(){return{...i()(this,g)}}}const v=y},14215:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});var r=n(61125),o=n.n(r);const s=class{constructor(){o()(this,"data",{})}register(e,t){this.data[e]=t}unregister(e){void 0===e?this.data={}:delete this.data[e]}get(e){return this.data[e]}}},84539:(e,t,n)=>{"use strict";n.r(t),n.d(t,{ALL_TYPES:()=>o,SCALAR_TYPES:()=>r});const r=["number","integer","string","boolean","null"],o=["array","object",...r]},13783:(e,t,n)=>{"use strict";n.r(t),n.d(t,{extractExample:()=>a,hasExample:()=>i});var r=n(58309),o=n.n(r),s=n(23084);const i=e=>{if(!(0,s.isJSONSchemaObject)(e))return!1;const{examples:t,example:n,default:r}=e;return!!(o()(t)&&t.length>=1)||(void 0!==r||void 0!==n)},a=e=>{if(!(0,s.isJSONSchemaObject)(e))return null;const{examples:t,example:n,default:r}=e;return o()(t)&&t.length>=1?t.at(0):void 0!==r?r:void 0!==n?n:void 0}},37078:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>v});var r=n(58309),o=n.n(r),s=n(39022),i=n.n(s),a=n(25110),l=n.n(a),c=n(82737),u=n.n(c),p=n(28222),h=n.n(p),f=n(14418),d=n.n(f),m=n(90242),g=n(23084);const y=function(e,t){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if((0,g.isBooleanJSONSchema)(e)&&!0===e)return!0;if((0,g.isBooleanJSONSchema)(e)&&!1===e)return!1;if((0,g.isBooleanJSONSchema)(t)&&!0===t)return!0;if((0,g.isBooleanJSONSchema)(t)&&!1===t)return!1;if(!(0,g.isJSONSchema)(e))return t;if(!(0,g.isJSONSchema)(t))return e;const r={...t,...e};if(t.type&&e.type&&o()(t.type)&&"string"==typeof t.type){var s;const n=i()(s=(0,m.AF)(t.type)).call(s,e.type);r.type=l()(new(u())(n))}if(o()(t.required)&&o()(e.required)&&(r.required=[...new(u())([...e.required,...t.required])]),t.properties&&e.properties){const o=new(u())([...h()(t.properties),...h()(e.properties)]);r.properties={};for(const s of o){const o=t.properties[s]||{},i=e.properties[s]||{};var a;if(o.readOnly&&!n.includeReadOnly||o.writeOnly&&!n.includeWriteOnly)r.required=d()(a=r.required||[]).call(a,(e=>e!==s));else r.properties[s]=y(i,o,n)}}return(0,g.isJSONSchema)(t.items)&&(0,g.isJSONSchema)(e.items)&&(r.items=y(e.items,t.items,n)),(0,g.isJSONSchema)(t.contains)&&(0,g.isJSONSchema)(e.contains)&&(r.contains=y(e.contains,t.contains,n)),(0,g.isJSONSchema)(t.contentSchema)&&(0,g.isJSONSchema)(e.contentSchema)&&(r.contentSchema=y(e.contentSchema,t.contentSchema,n)),r},v=y},23084:(e,t,n)=>{"use strict";n.r(t),n.d(t,{isBooleanJSONSchema:()=>s,isJSONSchema:()=>a,isJSONSchemaObject:()=>i});var r=n(68630),o=n.n(r);const s=e=>"boolean"==typeof e,i=e=>o()(e),a=e=>s(e)||i(e)},35202:(e,t,n)=>{"use strict";n.r(t),n.d(t,{bytes:()=>a,integer:()=>h,number:()=>p,pick:()=>c,randexp:()=>l,string:()=>u});var r=n(92282),o=n.n(r),s=n(14419),i=n.n(s);const a=e=>o()(e),l=e=>{try{return new(i())(e).gen()}catch{return"string"}},c=e=>e.at(0),u=()=>"string",p=()=>0,h=()=>0},96276:(e,t,n)=>{"use strict";n.r(t),n.d(t,{foldType:()=>_,getType:()=>O,inferType:()=>j});var r=n(58309),o=n.n(r),s=n(91086),i=n.n(s),a=n(58118),l=n.n(a),c=n(19030),u=n.n(c),p=n(28222),h=n.n(p),f=n(97606),d=n.n(f),m=n(14418),g=n.n(m),y=n(84539),v=n(23084),b=n(35202),w=n(13783);const E={array:["items","prefixItems","contains","maxContains","minContains","maxItems","minItems","uniqueItems","unevaluatedItems"],object:["properties","additionalProperties","patternProperties","propertyNames","minProperties","maxProperties","required","dependentSchemas","dependentRequired","unevaluatedProperties"],string:["pattern","format","minLength","maxLength","contentEncoding","contentMediaType","contentSchema"],integer:["minimum","maximum","exclusiveMinimum","exclusiveMaximum","multipleOf"]};E.number=E.integer;const x="string",S=e=>void 0===e?null:null===e?"null":o()(e)?"array":i()(e)?"integer":typeof e,_=e=>{if(o()(e)&&e.length>=1){if(l()(e).call(e,"array"))return"array";if(l()(e).call(e,"object"))return"object";{const t=(0,b.pick)(e);if(l()(y.ALL_TYPES).call(y.ALL_TYPES,t))return t}}return l()(y.ALL_TYPES).call(y.ALL_TYPES,e)?e:null},j=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:new(u());if(!(0,v.isJSONSchemaObject)(e))return x;if(t.has(e))return x;t.add(e);let{type:n,const:r}=e;if(n=_(n),"string"!=typeof n){const t=h()(E);e:for(let r=0;r{if(o()(e[n])){var r;const o=d()(r=e[n]).call(r,(e=>j(e,t)));return _(o)}return null},i=r("allOf"),a=r("anyOf"),l=r("oneOf"),c=e.not?j(e.not,t):null;var s;if(i||a||l||c)n=_(g()(s=[i,a,l,c]).call(s,Boolean))}if("string"!=typeof n&&(0,w.hasExample)(e)){const t=(0,w.extractExample)(e),r=S(t);n="string"==typeof r?r:n}return t.delete(e),n||x},O=e=>j(e)},99346:(e,t,n)=>{"use strict";n.r(t),n.d(t,{fromJSONBooleanSchema:()=>o,typeCast:()=>s});var r=n(23084);const o=e=>!1===e?{not:{}}:{},s=e=>(0,r.isBooleanJSONSchema)(e)?o(e):(0,r.isJSONSchemaObject)(e)?e:{}},41433:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(48764).Buffer;const o=e=>r.from(e).toString("ascii")},58509:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(48764).Buffer;const o=e=>r.from(e).toString("utf8")},5709:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(48764).Buffer;const o=e=>r.from(e).toString("hex")},54180:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(48764).Buffer;const o=e=>{const t=r.from(e).toString("utf8"),n="ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";let o=0,s="",i=0,a=0;for(let e=0;e=5;)s+=n.charAt(i>>>a-5&31),a-=5;a>0&&(s+=n.charAt(i<<5-a&31),o=(8-8*t.length%5)%5);for(let e=0;e{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(48764).Buffer;const o=e=>r.from(e).toString("base64")},44366:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(48764).Buffer;const o=e=>r.from(e).toString("binary")},65037:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});var r=n(24278),o=n.n(r);const s=e=>{let t="";for(let s=0;s=33&&i<=60||i>=62&&i<=126||9===i||32===i)t+=e.charAt(s);else if(13===i||10===i)t+="\r\n";else if(i>126){const r=unescape(encodeURIComponent(e.charAt(s)));for(let e=0;e{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>(new Date).toISOString()},81456:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>(new Date).toISOString().substring(0,10)},560:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>.1},64299:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>"P3D"},3981:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>"user@example.com"},51890:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>.1},69375:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>"example.com"},94518:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>"실례@example.com"},70273:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>"실례.com"},57864:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>2**30>>>0},21726:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>2**53-1},28793:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>"198.51.100.42"},98269:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>"2001:0db8:5b96:0000:0000:426f:8e17:642a"},45693:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>"path/실례.html"},13080:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>"https://실례.com/"},37856:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>"/a/b/c"},2672:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>i});var r=n(57740),o=n.n(r),s=n(35202);const i={"application/json":()=>'{"key":"value"}',"application/ld+json":()=>'{"name": "John Doe"}',"application/x-httpd-php":()=>"Hello World!

'; ?>","application/rtf":()=>o()`{\rtf1\adeflang1025\ansi\ansicpg1252\uc1`,"application/x-sh":()=>'echo "Hello World!"',"application/xhtml+xml":()=>"

content

","application/*":()=>(0,s.bytes)(25).toString("binary")}},54342:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(35202);const o={"audio/*":()=>(0,r.bytes)(25).toString("binary")}},46724:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(35202);const o={"image/*":()=>(0,r.bytes)(25).toString("binary")}},65378:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r={"text/plain":()=>"string","text/css":()=>".selector { border: 1px solid red }","text/csv":()=>"value1,value2,value3","text/html":()=>"

content

","text/calendar":()=>"BEGIN:VCALENDAR","text/javascript":()=>"console.dir('Hello world!');","text/xml":()=>'John Doe',"text/*":()=>"string"}},92974:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(35202);const o={"video/*":()=>(0,r.bytes)(25).toString("binary")}},93393:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>"********"},4335:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>"^[a-z]+$"},80375:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>"1/0"},65243:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>(new Date).toISOString().substring(11)},94692:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>"path/index.html"},83829:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>"https://example.com/dictionary/{term:1}/{term}"},52978:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>"https://example.com/"},38859:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>"3fa85f64-5717-4562-b3fc-2c963f66afa6"},78591:(e,t,n)=>{"use strict";n.r(t),n.d(t,{createXMLExample:()=>r.createXMLExample,encoderAPI:()=>o.default,formatAPI:()=>s.default,mediaTypeAPI:()=>i.default,memoizedCreateXMLExample:()=>r.memoizedCreateXMLExample,memoizedSampleFromSchema:()=>r.memoizedSampleFromSchema,sampleFromSchema:()=>r.sampleFromSchema,sampleFromSchemaGeneric:()=>r.sampleFromSchemaGeneric});var r=n(94277),o=n(9507),s=n(22906),i=n(90537)},94277:(e,t,n)=>{"use strict";n.r(t),n.d(t,{createXMLExample:()=>M,memoizedCreateXMLExample:()=>L,memoizedSampleFromSchema:()=>B,sampleFromSchema:()=>D,sampleFromSchemaGeneric:()=>R});var r=n(58309),o=n.n(r),s=n(91086),i=n.n(s),a=n(86),l=n.n(a),c=n(51679),u=n.n(c),p=n(58118),h=n.n(p),f=n(39022),d=n.n(f),m=n(97606),g=n.n(m),y=n(35627),v=n.n(y),b=n(53479),w=n.n(b),E=n(41609),x=n.n(E),S=n(68630),_=n.n(S),j=n(90242),O=n(60314),k=n(63273),A=n(96276),C=n(99346),P=n(13783),N=n(35202),I=n(37078),T=n(23084);const R=function(e){var t;let n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:void 0,s=arguments.length>3&&void 0!==arguments[3]&&arguments[3];"function"==typeof(null===(t=e)||void 0===t?void 0:t.toJS)&&(e=e.toJS()),e=(0,C.typeCast)(e);let a=void 0!==r||(0,P.hasExample)(e);const c=!a&&o()(e.oneOf)&&e.oneOf.length>0,p=!a&&o()(e.anyOf)&&e.anyOf.length>0;if(!a&&(c||p)){const t=(0,C.typeCast)(c?(0,N.pick)(e.oneOf):(0,N.pick)(e.anyOf));!(e=(0,I.default)(e,t,n)).xml&&t.xml&&(e.xml=t.xml),(0,P.hasExample)(e)&&(0,P.hasExample)(t)&&(a=!0)}const f={};let{xml:m,properties:y,additionalProperties:v,items:b,contains:w}=e||{},E=(0,A.getType)(e),{includeReadOnly:S,includeWriteOnly:O}=n;m=m||{};let M,{name:D,prefix:F,namespace:L}=m,B={};if(Object.hasOwn(e,"type")||(e.type=E),s&&(D=D||"notagname",M=(F?`${F}:`:"")+D,L)){f[F?`xmlns:${F}`:"xmlns"]=L}s&&(B[M]=[]);const $=(0,j.mz)(y);let q,U=0;const z=()=>i()(e.maxProperties)&&e.maxProperties>0&&U>=e.maxProperties,V=t=>!(i()(e.maxProperties)&&e.maxProperties>0)||!z()&&(!(t=>{var n;return!o()(e.required)||0===e.required.length||!h()(n=e.required).call(n,t)})(t)||e.maxProperties-U-(()=>{if(!o()(e.required)||0===e.required.length)return 0;let t=0;var n,r;return s?l()(n=e.required).call(n,(e=>t+=void 0===B[e]?0:1)):l()(r=e.required).call(r,(e=>{var n;t+=void 0===(null===(n=B[M])||void 0===n?void 0:u()(n).call(n,(t=>void 0!==t[e])))?0:1})),e.required.length-t})()>0);if(q=s?function(t){let r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:void 0;if(e&&$[t]){if($[t].xml=$[t].xml||{},$[t].xml.attribute){const e=o()($[t].enum)?(0,N.pick)($[t].enum):void 0;if((0,P.hasExample)($[t]))f[$[t].xml.name||t]=(0,P.extractExample)($[t]);else if(void 0!==e)f[$[t].xml.name||t]=e;else{const e=(0,C.typeCast)($[t]),n=(0,A.getType)(e),r=$[t].xml.name||t;f[r]=k.default[n](e)}return}$[t].xml.name=$[t].xml.name||t}else $[t]||!1===v||($[t]={xml:{name:t}});let i=R($[t],n,r,s);var a;V(t)&&(U++,o()(i)?B[M]=d()(a=B[M]).call(a,i):B[M].push(i))}:(t,r)=>{var o;if(V(t)){if(_()(null===(o=e.discriminator)||void 0===o?void 0:o.mapping)&&e.discriminator.propertyName===t&&"string"==typeof e.$$ref){for(const n in e.discriminator.mapping)if(-1!==e.$$ref.search(e.discriminator.mapping[n])){B[t]=n;break}}else B[t]=R($[t],n,r,s);U++}},a){let t;if(t=void 0!==r?r:(0,P.extractExample)(e),!s){if("number"==typeof t&&"string"===E)return`${t}`;if("string"!=typeof t||"string"===E)return t;try{return JSON.parse(t)}catch{return t}}if("array"===E){if(!o()(t)){if("string"==typeof t)return t;t=[t]}let r=[];return(0,T.isJSONSchemaObject)(b)&&(b.xml=b.xml||m||{},b.xml.name=b.xml.name||m.name,r=g()(t).call(t,(e=>R(b,n,e,s)))),(0,T.isJSONSchemaObject)(w)&&(w.xml=w.xml||m||{},w.xml.name=w.xml.name||m.name,r=[R(w,n,void 0,s),...r]),r=k.default.array(e,{sample:r}),m.wrapped?(B[M]=r,x()(f)||B[M].push({_attr:f})):B=r,B}if("object"===E){if("string"==typeof t)return t;for(const e in t){var W,J,K,H;Object.hasOwn(t,e)&&(null!==(W=$[e])&&void 0!==W&&W.readOnly&&!S||null!==(J=$[e])&&void 0!==J&&J.writeOnly&&!O||(null!==(K=$[e])&&void 0!==K&&null!==(H=K.xml)&&void 0!==H&&H.attribute?f[$[e].xml.name||e]=t[e]:q(e,t[e])))}return x()(f)||B[M].push({_attr:f}),B}return B[M]=x()(f)?t:[{_attr:f},t],B}if("array"===E){let t=[];var G,Z;if((0,T.isJSONSchemaObject)(w))if(s&&(w.xml=w.xml||e.xml||{},w.xml.name=w.xml.name||m.name),o()(w.anyOf))t.push(...g()(G=w.anyOf).call(G,(e=>R((0,I.default)(e,w,n),n,void 0,s))));else if(o()(w.oneOf)){var Y;t.push(...g()(Y=w.oneOf).call(Y,(e=>R((0,I.default)(e,w,n),n,void 0,s))))}else{if(!(!s||s&&m.wrapped))return R(w,n,void 0,s);t.push(R(w,n,void 0,s))}if((0,T.isJSONSchemaObject)(b))if(s&&(b.xml=b.xml||e.xml||{},b.xml.name=b.xml.name||m.name),o()(b.anyOf))t.push(...g()(Z=b.anyOf).call(Z,(e=>R((0,I.default)(e,b,n),n,void 0,s))));else if(o()(b.oneOf)){var X;t.push(...g()(X=b.oneOf).call(X,(e=>R((0,I.default)(e,b,n),n,void 0,s))))}else{if(!(!s||s&&m.wrapped))return R(b,n,void 0,s);t.push(R(b,n,void 0,s))}return t=k.default.array(e,{sample:t}),s&&m.wrapped?(B[M]=t,x()(f)||B[M].push({_attr:f}),B):t}if("object"===E){for(let e in $){var Q,ee,te;Object.hasOwn($,e)&&(null!==(Q=$[e])&&void 0!==Q&&Q.deprecated||null!==(ee=$[e])&&void 0!==ee&&ee.readOnly&&!S||null!==(te=$[e])&&void 0!==te&&te.writeOnly&&!O||q(e))}if(s&&f&&B[M].push({_attr:f}),z())return B;if((0,T.isBooleanJSONSchema)(v))s?B[M].push({additionalProp:"Anything can be here"}):B.additionalProp1={},U++;else if((0,T.isJSONSchemaObject)(v)){var ne,re;const t=v,r=R(t,n,void 0,s);if(s&&"string"==typeof(null==t||null===(ne=t.xml)||void 0===ne?void 0:ne.name)&&"notagname"!==(null==t||null===(re=t.xml)||void 0===re?void 0:re.name))B[M].push(r);else{const t=i()(e.minProperties)&&e.minProperties>0&&U{const r=R(e,t,n,!0);if(r)return"string"==typeof r?r:w()(r,{declaration:!0,indent:"\t"})},D=(e,t,n)=>R(e,t,n,!1),F=(e,t,n)=>[e,v()(t),v()(n)],L=(0,O.Z)(M,F),B=(0,O.Z)(D,F)},83982:(e,t,n)=>{"use strict";n.r(t),n.d(t,{applyArrayConstraints:()=>p,default:()=>h});var r=n(91086),o=n.n(r),s=n(24278),i=n.n(s),a=n(25110),l=n.n(a),c=n(82737),u=n.n(c);const p=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const{minItems:n,maxItems:r,uniqueItems:s}=t,{contains:a,minContains:c,maxContains:p}=t;let h=[...e];if(null!=a&&"object"==typeof a){if(o()(c)&&c>1){const e=h.at(0);for(let t=1;t0&&(h=i()(e).call(e,0,r)),o()(n)&&n>0)for(let e=0;h.length{let{sample:n}=t;return p(n,e)}},34108:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=e=>"boolean"!=typeof e.default||e.default},63273:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>p});var r=n(83982),o=n(46852),s=n(74522),i=n(83455),a=n(58864),l=n(34108),c=n(90853);const u={array:r.default,object:o.default,string:s.default,number:i.default,integer:a.default,boolean:l.default,null:c.default},p=new Proxy(u,{get:(e,t)=>"string"==typeof t&&Object.hasOwn(e,t)?e[t]:()=>`Unknown Type: ${t}`})},58864:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>a});var r=n(35202),o=n(22906),s=n(57864),i=n(21726);const a=e=>{const{format:t}=e;return"string"==typeof t?(e=>{const{format:t}=e,n=(0,o.default)(t);if("function"==typeof n)return n(e);switch(t){case"int32":return(0,s.default)();case"int64":return(0,i.default)()}return(0,r.integer)()})(e):(0,r.integer)()}},90853:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>null},83455:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>p});var r=n(91086),o=n.n(r),s=n(44081),i=n.n(s),a=n(35202),l=n(22906),c=n(51890),u=n(560);const p=e=>{const{format:t}=e;let n;return n="string"==typeof t?(e=>{const{format:t}=e,n=(0,l.default)(t);if("function"==typeof n)return n(e);switch(t){case"float":return(0,c.default)();case"double":return(0,u.default)()}return(0,a.number)()})(e):(0,a.number)(),function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const{minimum:n,maximum:r,exclusiveMinimum:s,exclusiveMaximum:a}=t,{multipleOf:l}=t,c=o()(e)?1:i();let u="number"==typeof n?n:null,p="number"==typeof r?r:null,h=e;if("number"==typeof s&&(u=null!==u?Math.max(u,s+c):s+c),"number"==typeof a&&(p=null!==p?Math.min(p,a-c):a-c),h=u>p&&e||u||p||h,"number"==typeof l&&l>0){const e=h%l;h=0===e?h:h+l-e}return h}(n,e)}},46852:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=()=>{throw new Error("Not implemented")}},74522:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>L});var r=n(91086),o=n.n(r),s=n(24278),i=n.n(s),a=n(58309),l=n.n(a),c=n(35627),u=n.n(c),p=n(6557),h=n.n(p),f=n(35202),d=n(23084),m=n(3981),g=n(94518),y=n(69375),v=n(70273),b=n(28793),w=n(98269),E=n(52978),x=n(94692),S=n(13080),_=n(45693),j=n(38859),O=n(83829),k=n(37856),A=n(80375),C=n(74045),P=n(81456),N=n(65243),I=n(64299),T=n(93393),R=n(4335),M=n(22906),D=n(9507),F=n(90537);const L=function(e){let{sample:t}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const{contentEncoding:n,contentMediaType:r,contentSchema:s}=e,{pattern:a,format:c}=e,p=(0,D.default)(n)||h();let L;if("string"==typeof a)L=(0,f.randexp)(a);else if("string"==typeof c)L=(e=>{const{format:t}=e,n=(0,M.default)(t);if("function"==typeof n)return n(e);switch(t){case"email":return(0,m.default)();case"idn-email":return(0,g.default)();case"hostname":return(0,y.default)();case"idn-hostname":return(0,v.default)();case"ipv4":return(0,b.default)();case"ipv6":return(0,w.default)();case"uri":return(0,E.default)();case"uri-reference":return(0,x.default)();case"iri":return(0,S.default)();case"iri-reference":return(0,_.default)();case"uuid":return(0,j.default)();case"uri-template":return(0,O.default)();case"json-pointer":return(0,k.default)();case"relative-json-pointer":return(0,A.default)();case"date-time":return(0,C.default)();case"date":return(0,P.default)();case"time":return(0,N.default)();case"duration":return(0,I.default)();case"password":return(0,T.default)();case"regex":return(0,R.default)()}return(0,f.string)()})(e);else if((0,d.isJSONSchema)(s)&&"string"==typeof r&&void 0!==t)L=l()(t)||"object"==typeof t?u()(t):String(t);else if("string"==typeof r){const t=(0,F.default)(r);"function"==typeof t&&(L=t(e))}else L=(0,f.string)();return p(function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const{maxLength:n,minLength:r}=t;let s=e;if(o()(n)&&n>0&&(s=i()(s).call(s,0,n)),o()(r)&&r>0){let e=0;for(;s.length{"use strict";n.r(t),n.d(t,{SHOW:()=>a,UPDATE_FILTER:()=>s,UPDATE_LAYOUT:()=>o,UPDATE_MODE:()=>i,changeMode:()=>p,show:()=>u,updateFilter:()=>c,updateLayout:()=>l});var r=n(90242);const o="layout_update_layout",s="layout_update_filter",i="layout_update_mode",a="layout_show";function l(e){return{type:o,payload:e}}function c(e){return{type:s,payload:e}}function u(e){let t=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];return e=(0,r.AF)(e),{type:a,payload:{thing:e,shown:t}}}function p(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";return e=(0,r.AF)(e),{type:i,payload:{thing:e,mode:t}}}},26821:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>a});var r=n(5672),o=n(25474),s=n(4400),i=n(28989);function a(){return{statePlugins:{layout:{reducers:r.default,actions:o,selectors:s},spec:{wrapSelectors:i}}}}},5672:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>a});var r=n(39022),o=n.n(r),s=n(43393),i=n(25474);const a={[i.UPDATE_LAYOUT]:(e,t)=>e.set("layout",t.payload),[i.UPDATE_FILTER]:(e,t)=>e.set("filter",t.payload),[i.SHOW]:(e,t)=>{const n=t.payload.shown,r=(0,s.fromJS)(t.payload.thing);return e.update("shown",(0,s.fromJS)({}),(e=>e.set(r,n)))},[i.UPDATE_MODE]:(e,t)=>{var n;let r=t.payload.thing,s=t.payload.mode;return e.setIn(o()(n=["modes"]).call(n,r),(s||"")+"")}}},4400:(e,t,n)=>{"use strict";n.r(t),n.d(t,{current:()=>i,currentFilter:()=>a,isShown:()=>l,showSummary:()=>u,whatMode:()=>c});var r=n(20573),o=n(90242),s=n(43393);const i=e=>e.get("layout"),a=e=>e.get("filter"),l=(e,t,n)=>(t=(0,o.AF)(t),e.get("shown",(0,s.fromJS)({})).get((0,s.fromJS)(t),n)),c=function(e,t){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"";return t=(0,o.AF)(t),e.getIn(["modes",...t],n)},u=(0,r.P1)((e=>e),(e=>!l(e,"editor")))},28989:(e,t,n)=>{"use strict";n.r(t),n.d(t,{taggedOperations:()=>s});var r=n(24278),o=n.n(r);const s=(e,t)=>function(n){for(var r=arguments.length,s=new Array(r>1?r-1:0),i=1;i=0&&(a=o()(a).call(a,0,h)),a}},9150:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});var r=n(11189),o=n.n(r);function s(e){let{configs:t}=e;const n={debug:0,info:1,log:2,warn:3,error:4},r=e=>n[e]||-1;let{logLevel:s}=t,i=r(s);function a(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),o=1;o=i&&console[e](...n)}return a.warn=o()(a).call(a,null,"warn"),a.error=o()(a).call(a,null,"error"),a.info=o()(a).call(a,null,"info"),a.debug=o()(a).call(a,null,"debug"),{rootInjects:{log:a}}}},67002:(e,t,n)=>{"use strict";n.r(t),n.d(t,{CLEAR_REQUEST_BODY_VALIDATE_ERROR:()=>h,CLEAR_REQUEST_BODY_VALUE:()=>f,SET_REQUEST_BODY_VALIDATE_ERROR:()=>p,UPDATE_ACTIVE_EXAMPLES_MEMBER:()=>a,UPDATE_REQUEST_BODY_INCLUSION:()=>i,UPDATE_REQUEST_BODY_VALUE:()=>o,UPDATE_REQUEST_BODY_VALUE_RETAIN_FLAG:()=>s,UPDATE_REQUEST_CONTENT_TYPE:()=>l,UPDATE_RESPONSE_CONTENT_TYPE:()=>c,UPDATE_SELECTED_SERVER:()=>r,UPDATE_SERVER_VARIABLE_VALUE:()=>u,clearRequestBodyValidateError:()=>S,clearRequestBodyValue:()=>j,initRequestBodyValidateError:()=>_,setActiveExamplesMember:()=>v,setRequestBodyInclusion:()=>y,setRequestBodyValidateError:()=>x,setRequestBodyValue:()=>m,setRequestContentType:()=>b,setResponseContentType:()=>w,setRetainRequestBodyValueFlag:()=>g,setSelectedServer:()=>d,setServerVariableValue:()=>E});const r="oas3_set_servers",o="oas3_set_request_body_value",s="oas3_set_request_body_retain_flag",i="oas3_set_request_body_inclusion",a="oas3_set_active_examples_member",l="oas3_set_request_content_type",c="oas3_set_response_content_type",u="oas3_set_server_variable_value",p="oas3_set_request_body_validate_error",h="oas3_clear_request_body_validate_error",f="oas3_clear_request_body_value";function d(e,t){return{type:r,payload:{selectedServerUrl:e,namespace:t}}}function m(e){let{value:t,pathMethod:n}=e;return{type:o,payload:{value:t,pathMethod:n}}}const g=e=>{let{value:t,pathMethod:n}=e;return{type:s,payload:{value:t,pathMethod:n}}};function y(e){let{value:t,pathMethod:n,name:r}=e;return{type:i,payload:{value:t,pathMethod:n,name:r}}}function v(e){let{name:t,pathMethod:n,contextType:r,contextName:o}=e;return{type:a,payload:{name:t,pathMethod:n,contextType:r,contextName:o}}}function b(e){let{value:t,pathMethod:n}=e;return{type:l,payload:{value:t,pathMethod:n}}}function w(e){let{value:t,path:n,method:r}=e;return{type:c,payload:{value:t,path:n,method:r}}}function E(e){let{server:t,namespace:n,key:r,val:o}=e;return{type:u,payload:{server:t,namespace:n,key:r,val:o}}}const x=e=>{let{path:t,method:n,validationErrors:r}=e;return{type:p,payload:{path:t,method:n,validationErrors:r}}},S=e=>{let{path:t,method:n}=e;return{type:h,payload:{path:t,method:n}}},_=e=>{let{pathMethod:t}=e;return{type:h,payload:{path:t[0],method:t[1]}}},j=e=>{let{pathMethod:t}=e;return{type:f,payload:{pathMethod:t}}}},73723:(e,t,n)=>{"use strict";n.r(t),n.d(t,{definitionsToAuthorize:()=>p});var r=n(86),o=n.n(r),s=n(14418),i=n.n(s),a=n(24282),l=n.n(a),c=n(20573),u=n(43393);const p=(h=(0,c.P1)((e=>e),(e=>{let{specSelectors:t}=e;return t.securityDefinitions()}),((e,t)=>{var n;let r=(0,u.List)();return t?(o()(n=t.entrySeq()).call(n,(e=>{let[t,n]=e;const s=n.get("type");var a;if("oauth2"===s&&o()(a=n.get("flows").entrySeq()).call(a,(e=>{let[o,s]=e,a=(0,u.fromJS)({flow:o,authorizationUrl:s.get("authorizationUrl"),tokenUrl:s.get("tokenUrl"),scopes:s.get("scopes"),type:n.get("type"),description:n.get("description")});r=r.push(new u.Map({[t]:i()(a).call(a,(e=>void 0!==e))}))})),"http"!==s&&"apiKey"!==s||(r=r.push(new u.Map({[t]:n}))),"openIdConnect"===s&&n.get("openIdConnectData")){let e=n.get("openIdConnectData"),s=e.get("grant_types_supported")||["authorization_code","implicit"];o()(s).call(s,(o=>{var s;let a=e.get("scopes_supported")&&l()(s=e.get("scopes_supported")).call(s,((e,t)=>e.set(t,"")),new u.Map),c=(0,u.fromJS)({flow:o,authorizationUrl:e.get("authorization_endpoint"),tokenUrl:e.get("token_endpoint"),scopes:a,type:"oauth2",openIdConnectUrl:n.get("openIdConnectUrl")});r=r.push(new u.Map({[t]:i()(c).call(c,(e=>void 0!==e))}))}))}})),r):r})),(e,t)=>function(){for(var n=arguments.length,r=new Array(n),o=0;o{"use strict";n.r(t),n.d(t,{default:()=>l});var r=n(28222),o=n.n(r),s=n(97606),i=n.n(s),a=n(67294);n(23930);const l=e=>{let{callbacks:t,specPath:n,specSelectors:r,getComponent:s}=e;const l=r.callbacksOperations({callbacks:t,specPath:n}),c=o()(l),u=s("OperationContainer",!0);return 0===c.length?a.createElement("span",null,"No callbacks"):a.createElement("div",null,i()(c).call(c,(e=>{var t;return a.createElement("div",{key:`${e}`},a.createElement("h2",null,e),i()(t=l[e]).call(t,(t=>a.createElement(u,{key:`${e}-${t.path}-${t.method}`,op:t.operation,tag:"callbacks",method:t.method,path:t.path,specPath:t.specPath,allowTryItOut:!1}))))})))}},86775:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>h});var r=n(61125),o=n.n(r),s=n(76986),i=n.n(s),a=n(14418),l=n.n(a),c=n(97606),u=n.n(c),p=n(67294);class h extends p.Component{constructor(e,t){super(e,t),o()(this,"onChange",(e=>{let{onChange:t}=this.props,{value:n,name:r}=e.target,o=i()({},this.state.value);r?o[r]=n:o=n,this.setState({value:o},(()=>t(this.state)))}));let{name:n,schema:r}=this.props,s=this.getValue();this.state={name:n,schema:r,value:s}}getValue(){let{name:e,authorized:t}=this.props;return t&&t.getIn([e,"value"])}render(){var e;let{schema:t,getComponent:n,errSelectors:r,name:o}=this.props;const s=n("Input"),i=n("Row"),a=n("Col"),c=n("authError"),h=n("Markdown",!0),f=n("JumpToPath",!0),d=(t.get("scheme")||"").toLowerCase();let m=this.getValue(),g=l()(e=r.allErrors()).call(e,(e=>e.get("authId")===o));if("basic"===d){var y;let e=m?m.get("username"):null;return p.createElement("div",null,p.createElement("h4",null,p.createElement("code",null,o||t.get("name")),"  (http, Basic)",p.createElement(f,{path:["securityDefinitions",o]})),e&&p.createElement("h6",null,"Authorized"),p.createElement(i,null,p.createElement(h,{source:t.get("description")})),p.createElement(i,null,p.createElement("label",null,"Username:"),e?p.createElement("code",null," ",e," "):p.createElement(a,null,p.createElement(s,{type:"text",required:"required",name:"username","aria-label":"auth-basic-username",onChange:this.onChange,autoFocus:!0}))),p.createElement(i,null,p.createElement("label",null,"Password:"),e?p.createElement("code",null," ****** "):p.createElement(a,null,p.createElement(s,{autoComplete:"new-password",name:"password",type:"password","aria-label":"auth-basic-password",onChange:this.onChange}))),u()(y=g.valueSeq()).call(y,((e,t)=>p.createElement(c,{error:e,key:t}))))}var v;return"bearer"===d?p.createElement("div",null,p.createElement("h4",null,p.createElement("code",null,o||t.get("name")),"  (http, Bearer)",p.createElement(f,{path:["securityDefinitions",o]})),m&&p.createElement("h6",null,"Authorized"),p.createElement(i,null,p.createElement(h,{source:t.get("description")})),p.createElement(i,null,p.createElement("label",null,"Value:"),m?p.createElement("code",null," ****** "):p.createElement(a,null,p.createElement(s,{type:"text","aria-label":"auth-bearer-value",onChange:this.onChange,autoFocus:!0}))),u()(v=g.valueSeq()).call(v,((e,t)=>p.createElement(c,{error:e,key:t})))):p.createElement("div",null,p.createElement("em",null,p.createElement("b",null,o)," HTTP authentication: unsupported scheme ",`'${d}'`))}}},76467:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>p});var r=n(33427),o=n(42458),s=n(15757),i=n(56617),a=n(9928),l=n(45327),c=n(86775),u=n(96796);const p={Callbacks:r.default,HttpAuth:c.default,RequestBody:o.default,Servers:i.default,ServersContainer:a.default,RequestBodyEditor:l.default,OperationServers:u.default,operationLink:s.default}},15757:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>c});var r=n(35627),o=n.n(r),s=n(97606),i=n.n(s),a=n(67294);n(23930);class l extends a.Component{render(){const{link:e,name:t,getComponent:n}=this.props,r=n("Markdown",!0);let s=e.get("operationId")||e.get("operationRef"),l=e.get("parameters")&&e.get("parameters").toJS(),c=e.get("description");return a.createElement("div",{className:"operation-link"},a.createElement("div",{className:"description"},a.createElement("b",null,a.createElement("code",null,t)),c?a.createElement(r,{source:c}):null),a.createElement("pre",null,"Operation `",s,"`",a.createElement("br",null),a.createElement("br",null),"Parameters ",function(e,t){var n;if("string"!=typeof t)return"";return i()(n=t.split("\n")).call(n,((t,n)=>n>0?Array(e+1).join(" ")+t:t)).join("\n")}(0,o()(l,null,2))||"{}",a.createElement("br",null)))}}const c=l},96796:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>i});var r=n(61125),o=n.n(r),s=n(67294);n(23930);class i extends s.Component{constructor(){super(...arguments),o()(this,"setSelectedServer",(e=>{const{path:t,method:n}=this.props;return this.forceUpdate(),this.props.setSelectedServer(e,`${t}:${n}`)})),o()(this,"setServerVariableValue",(e=>{const{path:t,method:n}=this.props;return this.forceUpdate(),this.props.setServerVariableValue({...e,namespace:`${t}:${n}`})})),o()(this,"getSelectedServer",(()=>{const{path:e,method:t}=this.props;return this.props.getSelectedServer(`${e}:${t}`)})),o()(this,"getServerVariable",((e,t)=>{const{path:n,method:r}=this.props;return this.props.getServerVariable({namespace:`${n}:${r}`,server:e},t)})),o()(this,"getEffectiveServerValue",(e=>{const{path:t,method:n}=this.props;return this.props.getEffectiveServerValue({server:e,namespace:`${t}:${n}`})}))}render(){const{operationServers:e,pathServers:t,getComponent:n}=this.props;if(!e&&!t)return null;const r=n("Servers"),o=e||t,i=e?"operation":"path";return s.createElement("div",{className:"opblock-section operation-servers"},s.createElement("div",{className:"opblock-section-header"},s.createElement("div",{className:"tab-header"},s.createElement("h4",{className:"opblock-title"},"Servers"))),s.createElement("div",{className:"opblock-description-wrapper"},s.createElement("h4",{className:"message"},"These ",i,"-level options override the global server options."),s.createElement(r,{servers:o,currentServer:this.getSelectedServer(),setSelectedServer:this.setSelectedServer,setServerVariableValue:this.setServerVariableValue,getServerVariable:this.getServerVariable,getEffectiveServerValue:this.getEffectiveServerValue})))}}},45327:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>u});var r=n(61125),o=n.n(r),s=n(67294),i=n(94184),a=n.n(i),l=n(90242);const c=Function.prototype;class u extends s.PureComponent{constructor(e,t){super(e,t),o()(this,"applyDefaultValue",(e=>{const{onChange:t,defaultValue:n}=e||this.props;return this.setState({value:n}),t(n)})),o()(this,"onChange",(e=>{this.props.onChange((0,l.Pz)(e))})),o()(this,"onDomChange",(e=>{const t=e.target.value;this.setState({value:t},(()=>this.onChange(t)))})),this.state={value:(0,l.Pz)(e.value)||e.defaultValue},e.onChange(e.value)}UNSAFE_componentWillReceiveProps(e){this.props.value!==e.value&&e.value!==this.state.value&&this.setState({value:(0,l.Pz)(e.value)}),!e.value&&e.defaultValue&&this.state.value&&this.applyDefaultValue(e)}render(){let{getComponent:e,errors:t}=this.props,{value:n}=this.state,r=t.size>0;const o=e("TextArea");return s.createElement("div",{className:"body-param"},s.createElement(o,{className:a()("body-param__text",{invalid:r}),title:t.size?t.join(", "):"",value:n,onChange:this.onDomChange}))}}o()(u,"defaultProps",{onChange:c,userHasEditedBody:!1})},42458:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>g,getDefaultRequestBodyValue:()=>m});var r=n(97606),o=n.n(r),s=n(11882),i=n.n(s),a=n(58118),l=n.n(a),c=n(58309),u=n.n(c),p=n(67294),h=(n(23930),n(43393)),f=n(90242),d=n(2518);const m=(e,t,n,r)=>{const o=e.getIn(["content",t]),s=o.get("schema").toJS(),i=void 0!==o.get("examples"),a=o.get("example"),l=i?o.getIn(["examples",n,"value"]):a,c=r.getSampleSchema(s,t,{includeWriteOnly:!0},l);return(0,f.Pz)(c)},g=e=>{let{userHasEditedBody:t,requestBody:n,requestBodyValue:r,requestBodyInclusionSetting:s,requestBodyErrors:a,getComponent:c,getConfigs:g,specSelectors:y,fn:v,contentType:b,isExecute:w,specPath:E,onChange:x,onChangeIncludeEmpty:S,activeExamplesKey:_,updateActiveExamplesKey:j,setRetainRequestBodyValueFlag:O}=e;const k=e=>{x(e.target.files[0])},A=e=>{let t={key:e,shouldDispatchInit:!1,defaultValue:!0};return"no value"===s.get(e,"no value")&&(t.shouldDispatchInit=!0),t},C=c("Markdown",!0),P=c("modelExample"),N=c("RequestBodyEditor"),I=c("highlightCode"),T=c("ExamplesSelectValueRetainer"),R=c("Example"),M=c("ParameterIncludeEmpty"),{showCommonExtensions:D}=g(),F=n&&n.get("description")||null,L=n&&n.get("content")||new h.OrderedMap;b=b||L.keySeq().first()||"";const B=L.get(b,(0,h.OrderedMap)()),$=B.get("schema",(0,h.OrderedMap)()),q=B.get("examples",null),U=null==q?void 0:o()(q).call(q,((e,t)=>{var r;const o=null===(r=e)||void 0===r?void 0:r.get("value",null);return o&&(e=e.set("value",m(n,b,t,v),o)),e}));if(a=h.List.isList(a)?a:(0,h.List)(),!B.size)return null;const z="object"===B.getIn(["schema","type"]),V="binary"===B.getIn(["schema","format"]),W="base64"===B.getIn(["schema","format"]);if("application/octet-stream"===b||0===i()(b).call(b,"image/")||0===i()(b).call(b,"audio/")||0===i()(b).call(b,"video/")||V||W){const e=c("Input");return w?p.createElement(e,{type:"file",onChange:k}):p.createElement("i",null,"Example values are not available for ",p.createElement("code",null,b)," media types.")}if(z&&("application/x-www-form-urlencoded"===b||0===i()(b).call(b,"multipart/"))&&$.get("properties",(0,h.OrderedMap)()).size>0){var J;const e=c("JsonSchemaForm"),t=c("ParameterExt"),n=$.get("properties",(0,h.OrderedMap)());return r=h.Map.isMap(r)?r:(0,h.OrderedMap)(),p.createElement("div",{className:"table-container"},F&&p.createElement(C,{source:F}),p.createElement("table",null,p.createElement("tbody",null,h.Map.isMap(n)&&o()(J=n.entrySeq()).call(J,(n=>{var i,d;let[m,g]=n;if(g.get("readOnly"))return;let y=D?(0,f.po)(g):null;const b=l()(i=$.get("required",(0,h.List)())).call(i,m),E=g.get("type"),_=g.get("format"),j=g.get("description"),O=r.getIn([m,"value"]),k=r.getIn([m,"errors"])||a,P=s.get(m)||!1,N=g.has("default")||g.has("example")||g.hasIn(["items","example"])||g.hasIn(["items","default"]),I=g.has("enum")&&(1===g.get("enum").size||b),T=N||I;let R="";"array"!==E||T||(R=[]),("object"===E||T)&&(R=v.getSampleSchema(g,!1,{includeWriteOnly:!0})),"string"!=typeof R&&"object"===E&&(R=(0,f.Pz)(R)),"string"==typeof R&&"array"===E&&(R=JSON.parse(R));const F="string"===E&&("binary"===_||"base64"===_);return p.createElement("tr",{key:m,className:"parameters","data-property-name":m},p.createElement("td",{className:"parameters-col_name"},p.createElement("div",{className:b?"parameter__name required":"parameter__name"},m,b?p.createElement("span",null," *"):null),p.createElement("div",{className:"parameter__type"},E,_&&p.createElement("span",{className:"prop-format"},"($",_,")"),D&&y.size?o()(d=y.entrySeq()).call(d,(e=>{let[n,r]=e;return p.createElement(t,{key:`${n}-${r}`,xKey:n,xVal:r})})):null),p.createElement("div",{className:"parameter__deprecated"},g.get("deprecated")?"deprecated":null)),p.createElement("td",{className:"parameters-col_description"},p.createElement(C,{source:j}),w?p.createElement("div",null,p.createElement(e,{fn:v,dispatchInitialValue:!F,schema:g,description:m,getComponent:c,value:void 0===O?R:O,required:b,errors:k,onChange:e=>{x(e,[m])}}),b?null:p.createElement(M,{onChange:e=>S(m,e),isIncluded:P,isIncludedOptions:A(m),isDisabled:u()(O)?0!==O.length:!(0,f.O2)(O)})):null))})))))}const K=m(n,b,_,v);let H=null;return(0,d.O)(K)&&(H="json"),p.createElement("div",null,F&&p.createElement(C,{source:F}),U?p.createElement(T,{userHasEditedBody:t,examples:U,currentKey:_,currentUserInputValue:r,onSelect:e=>{j(e)},updateValue:x,defaultToFirstExample:!0,getComponent:c,setRetainRequestBodyValueFlag:O}):null,w?p.createElement("div",null,p.createElement(N,{value:r,errors:a,defaultValue:K,onChange:x,getComponent:c})):p.createElement(P,{getComponent:c,getConfigs:g,specSelectors:y,expandDepth:1,isExecute:w,schema:B.get("schema"),specPath:E.push("content",b),example:p.createElement(I,{className:"body-param__example",getConfigs:g,language:H,value:(0,f.Pz)(r)||K}),includeWriteOnly:!0}),U?p.createElement(R,{example:U.get(_),getComponent:c,getConfigs:g}):null)}},9928:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);class o extends r.Component{render(){const{specSelectors:e,oas3Selectors:t,oas3Actions:n,getComponent:o}=this.props,s=e.servers(),i=o("Servers");return s&&s.size?r.createElement("div",null,r.createElement("span",{className:"servers-title"},"Servers"),r.createElement(i,{servers:s,currentServer:t.selectedServer(),setSelectedServer:n.setSelectedServer,setServerVariableValue:n.setServerVariableValue,getServerVariable:t.serverVariableValue,getEffectiveServerValue:t.serverEffectiveValue})):null}}},56617:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>p});var r=n(61125),o=n.n(r),s=n(51679),i=n.n(s),a=n(97606),l=n.n(a),c=n(67294),u=n(43393);n(23930);class p extends c.Component{constructor(){super(...arguments),o()(this,"onServerChange",(e=>{this.setServer(e.target.value)})),o()(this,"onServerVariableValueChange",(e=>{let{setServerVariableValue:t,currentServer:n}=this.props,r=e.target.getAttribute("data-variable"),o=e.target.value;"function"==typeof t&&t({server:n,key:r,val:o})})),o()(this,"setServer",(e=>{let{setSelectedServer:t}=this.props;t(e)}))}componentDidMount(){var e;let{servers:t,currentServer:n}=this.props;n||this.setServer(null===(e=t.first())||void 0===e?void 0:e.get("url"))}UNSAFE_componentWillReceiveProps(e){let{servers:t,setServerVariableValue:n,getServerVariable:r}=e;if(this.props.currentServer!==e.currentServer||this.props.servers!==e.servers){var o;let s=i()(t).call(t,(t=>t.get("url")===e.currentServer)),a=i()(o=this.props.servers).call(o,(e=>e.get("url")===this.props.currentServer))||(0,u.OrderedMap)();if(!s)return this.setServer(t.first().get("url"));let c=a.get("variables")||(0,u.OrderedMap)(),p=(i()(c).call(c,(e=>e.get("default")))||(0,u.OrderedMap)()).get("default"),h=s.get("variables")||(0,u.OrderedMap)(),f=(i()(h).call(h,(e=>e.get("default")))||(0,u.OrderedMap)()).get("default");l()(h).call(h,((t,o)=>{r(e.currentServer,o)&&p===f||n({server:e.currentServer,key:o,val:t.get("default")||""})}))}}render(){var e,t;let{servers:n,currentServer:r,getServerVariable:o,getEffectiveServerValue:s}=this.props,a=(i()(n).call(n,(e=>e.get("url")===r))||(0,u.OrderedMap)()).get("variables")||(0,u.OrderedMap)(),p=0!==a.size;return c.createElement("div",{className:"servers"},c.createElement("label",{htmlFor:"servers"},c.createElement("select",{onChange:this.onServerChange,value:r},l()(e=n.valueSeq()).call(e,(e=>c.createElement("option",{value:e.get("url"),key:e.get("url")},e.get("url"),e.get("description")&&` - ${e.get("description")}`))).toArray())),p?c.createElement("div",null,c.createElement("div",{className:"computed-url"},"Computed URL:",c.createElement("code",null,s(r))),c.createElement("h4",null,"Server variables"),c.createElement("table",null,c.createElement("tbody",null,l()(t=a.entrySeq()).call(t,(e=>{var t;let[n,s]=e;return c.createElement("tr",{key:n},c.createElement("td",null,n),c.createElement("td",null,s.get("enum")?c.createElement("select",{"data-variable":n,onChange:this.onServerVariableValueChange},l()(t=s.get("enum")).call(t,(e=>c.createElement("option",{selected:e===o(r,n),key:e,value:e},e)))):c.createElement("input",{type:"text",value:o(r,n)||"",onChange:this.onServerVariableValueChange,"data-variable":n})))}))))):null)}}},7779:(e,t,n)=>{"use strict";n.r(t),n.d(t,{OAS30ComponentWrapFactory:()=>c,OAS3ComponentWrapFactory:()=>l,isOAS30:()=>i,isSwagger2:()=>a});var r=n(23101),o=n.n(r),s=n(67294);function i(e){const t=e.get("openapi");return"string"==typeof t&&/^3\.0\.([0123])(?:-rc[012])?$/.test(t)}function a(e){const t=e.get("swagger");return"string"==typeof t&&"2.0"===t}function l(e){return(t,n)=>r=>{var i;return"function"==typeof(null===(i=n.specSelectors)||void 0===i?void 0:i.isOAS3)?n.specSelectors.isOAS3()?s.createElement(e,o()({},r,n,{Ori:t})):s.createElement(t,r):(console.warn("OAS3 wrapper: couldn't get spec"),null)}}function c(e){return(t,n)=>r=>{var i;return"function"==typeof(null===(i=n.specSelectors)||void 0===i?void 0:i.isOAS30)?n.specSelectors.isOAS30()?s.createElement(e,o()({},r,n,{Ori:t})):s.createElement(t,r):(console.warn("OAS30 wrapper: couldn't get spec"),null)}}},97451:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>p});var r=n(92044),o=n(73723),s=n(91741),i=n(76467),a=n(37761),l=n(67002),c=n(5065),u=n(62109);function p(){return{components:i.default,wrapComponents:a.default,statePlugins:{spec:{wrapSelectors:r,selectors:s},auth:{wrapSelectors:o},oas3:{actions:l,reducers:u.default,selectors:c}}}}},62109:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>p});var r=n(8712),o=n.n(r),s=n(86),i=n.n(s),a=n(24282),l=n.n(a),c=n(43393),u=n(67002);const p={[u.UPDATE_SELECTED_SERVER]:(e,t)=>{let{payload:{selectedServerUrl:n,namespace:r}}=t;const o=r?[r,"selectedServer"]:["selectedServer"];return e.setIn(o,n)},[u.UPDATE_REQUEST_BODY_VALUE]:(e,t)=>{let{payload:{value:n,pathMethod:r}}=t,[s,a]=r;if(!c.Map.isMap(n))return e.setIn(["requestData",s,a,"bodyValue"],n);let l,u=e.getIn(["requestData",s,a,"bodyValue"])||(0,c.Map)();c.Map.isMap(u)||(u=(0,c.Map)());const[...p]=o()(n).call(n);return i()(p).call(p,(e=>{let t=n.getIn([e]);u.has(e)&&c.Map.isMap(t)||(l=u.setIn([e,"value"],t))})),e.setIn(["requestData",s,a,"bodyValue"],l)},[u.UPDATE_REQUEST_BODY_VALUE_RETAIN_FLAG]:(e,t)=>{let{payload:{value:n,pathMethod:r}}=t,[o,s]=r;return e.setIn(["requestData",o,s,"retainBodyValue"],n)},[u.UPDATE_REQUEST_BODY_INCLUSION]:(e,t)=>{let{payload:{value:n,pathMethod:r,name:o}}=t,[s,i]=r;return e.setIn(["requestData",s,i,"bodyInclusion",o],n)},[u.UPDATE_ACTIVE_EXAMPLES_MEMBER]:(e,t)=>{let{payload:{name:n,pathMethod:r,contextType:o,contextName:s}}=t,[i,a]=r;return e.setIn(["examples",i,a,o,s,"activeExample"],n)},[u.UPDATE_REQUEST_CONTENT_TYPE]:(e,t)=>{let{payload:{value:n,pathMethod:r}}=t,[o,s]=r;return e.setIn(["requestData",o,s,"requestContentType"],n)},[u.UPDATE_RESPONSE_CONTENT_TYPE]:(e,t)=>{let{payload:{value:n,path:r,method:o}}=t;return e.setIn(["requestData",r,o,"responseContentType"],n)},[u.UPDATE_SERVER_VARIABLE_VALUE]:(e,t)=>{let{payload:{server:n,namespace:r,key:o,val:s}}=t;const i=r?[r,"serverVariableValues",n,o]:["serverVariableValues",n,o];return e.setIn(i,s)},[u.SET_REQUEST_BODY_VALIDATE_ERROR]:(e,t)=>{let{payload:{path:n,method:r,validationErrors:o}}=t,s=[];if(s.push("Required field is not provided"),o.missingBodyValue)return e.setIn(["requestData",n,r,"errors"],(0,c.fromJS)(s));if(o.missingRequiredKeys&&o.missingRequiredKeys.length>0){const{missingRequiredKeys:t}=o;return e.updateIn(["requestData",n,r,"bodyValue"],(0,c.fromJS)({}),(e=>l()(t).call(t,((e,t)=>e.setIn([t,"errors"],(0,c.fromJS)(s))),e)))}return console.warn("unexpected result: SET_REQUEST_BODY_VALIDATE_ERROR"),e},[u.CLEAR_REQUEST_BODY_VALIDATE_ERROR]:(e,t)=>{let{payload:{path:n,method:r}}=t;const s=e.getIn(["requestData",n,r,"bodyValue"]);if(!c.Map.isMap(s))return e.setIn(["requestData",n,r,"errors"],(0,c.fromJS)([]));const[...i]=o()(s).call(s);return i?e.updateIn(["requestData",n,r,"bodyValue"],(0,c.fromJS)({}),(e=>l()(i).call(i,((e,t)=>e.setIn([t,"errors"],(0,c.fromJS)([]))),e))):e},[u.CLEAR_REQUEST_BODY_VALUE]:(e,t)=>{let{payload:{pathMethod:n}}=t,[r,o]=n;const s=e.getIn(["requestData",r,o,"bodyValue"]);return s?c.Map.isMap(s)?e.setIn(["requestData",r,o,"bodyValue"],(0,c.Map)()):e.setIn(["requestData",r,o,"bodyValue"],""):e}}},5065:(e,t,n)=>{"use strict";n.r(t),n.d(t,{activeExamplesMember:()=>S,hasUserEditedBody:()=>w,requestBodyErrors:()=>x,requestBodyInclusionSetting:()=>E,requestBodyValue:()=>y,requestContentType:()=>_,responseContentType:()=>j,selectDefaultRequestBodyValue:()=>b,selectedServer:()=>g,serverEffectiveValue:()=>A,serverVariableValue:()=>O,serverVariables:()=>k,shouldRetainRequestBodyValue:()=>v,validOperationMethods:()=>I,validateBeforeExecute:()=>C,validateShallowRequired:()=>N});var r=n(97606),o=n.n(r),s=n(86),i=n.n(s),a=n(28222),l=n.n(a),c=n(11882),u=n.n(c),p=n(43393),h=n(20573),f=n(42458),d=n(90242);const m=e=>function(t){for(var n=arguments.length,r=new Array(n>1?n-1:0),o=1;o{if(n.getSystem().specSelectors.isOAS3()){const o=e(t,...r);return"function"==typeof o?o(n):o}return null}};const g=m(((e,t)=>{const n=t?[t,"selectedServer"]:["selectedServer"];return e.getIn(n)||""})),y=m(((e,t,n)=>e.getIn(["requestData",t,n,"bodyValue"])||null)),v=m(((e,t,n)=>e.getIn(["requestData",t,n,"retainBodyValue"])||!1)),b=(e,t,n)=>e=>{const{oas3Selectors:r,specSelectors:o,fn:s}=e.getSystem();if(o.isOAS3()){const e=r.requestContentType(t,n);if(e)return(0,f.getDefaultRequestBodyValue)(o.specResolvedSubtree(["paths",t,n,"requestBody"]),e,r.activeExamplesMember(t,n,"requestBody","requestBody"),s)}return null},w=m(((e,t,n)=>e=>{const{oas3Selectors:r,specSelectors:o,fn:s}=e;let i=!1;const a=r.requestContentType(t,n);let l=r.requestBodyValue(t,n);const c=o.specResolvedSubtree(["paths",t,n,"requestBody"]);if(!c)return!1;if(p.Map.isMap(l)&&(l=(0,d.Pz)(l.mapEntries((e=>p.Map.isMap(e[1])?[e[0],e[1].get("value")]:e)).toJS())),p.List.isList(l)&&(l=(0,d.Pz)(l)),a){const e=(0,f.getDefaultRequestBodyValue)(c,a,r.activeExamplesMember(t,n,"requestBody","requestBody"),s);i=!!l&&l!==e}return i})),E=m(((e,t,n)=>e.getIn(["requestData",t,n,"bodyInclusion"])||(0,p.Map)())),x=m(((e,t,n)=>e.getIn(["requestData",t,n,"errors"])||null)),S=m(((e,t,n,r,o)=>e.getIn(["examples",t,n,r,o,"activeExample"])||null)),_=m(((e,t,n)=>e.getIn(["requestData",t,n,"requestContentType"])||null)),j=m(((e,t,n)=>e.getIn(["requestData",t,n,"responseContentType"])||null)),O=m(((e,t,n)=>{let r;if("string"!=typeof t){const{server:e,namespace:o}=t;r=o?[o,"serverVariableValues",e,n]:["serverVariableValues",e,n]}else{r=["serverVariableValues",t,n]}return e.getIn(r)||null})),k=m(((e,t)=>{let n;if("string"!=typeof t){const{server:e,namespace:r}=t;n=r?[r,"serverVariableValues",e]:["serverVariableValues",e]}else{n=["serverVariableValues",t]}return e.getIn(n)||(0,p.OrderedMap)()})),A=m(((e,t)=>{var n,r;if("string"!=typeof t){const{server:o,namespace:s}=t;r=o,n=s?e.getIn([s,"serverVariableValues",r]):e.getIn(["serverVariableValues",r])}else r=t,n=e.getIn(["serverVariableValues",r]);n=n||(0,p.OrderedMap)();let s=r;return o()(n).call(n,((e,t)=>{s=s.replace(new RegExp(`{${t}}`,"g"),e)})),s})),C=(P=(e,t)=>((e,t)=>(t=t||[],!!e.getIn(["requestData",...t,"bodyValue"])))(e,t),function(){for(var e=arguments.length,t=new Array(e),n=0;n{const n=e.getSystem().specSelectors.specJson();let r=[...t][1]||[];return!n.getIn(["paths",...r,"requestBody","required"])||P(...t)}});var P;const N=(e,t)=>{var n;let{oas3RequiredRequestBodyContentType:r,oas3RequestContentType:o,oas3RequestBodyValue:s}=t,a=[];if(!p.Map.isMap(s))return a;let c=[];return i()(n=l()(r.requestContentType)).call(n,(e=>{if(e===o){let t=r.requestContentType[e];i()(t).call(t,(e=>{u()(c).call(c,e)<0&&c.push(e)}))}})),i()(c).call(c,(e=>{s.getIn([e,"value"])||a.push(e)})),a},I=(0,h.P1)((()=>["get","put","post","delete","options","head","patch","trace"]))},91741:(e,t,n)=>{"use strict";n.r(t),n.d(t,{callbacksOperations:()=>E,isOAS3:()=>v,isOAS30:()=>y,isSwagger2:()=>g,servers:()=>w});var r=n(97606),o=n.n(r),s=n(24282),i=n.n(s),a=n(14418),l=n.n(a),c=n(58118),u=n.n(c),p=n(39022),h=n.n(p),f=n(43393),d=n(7779);const m=(0,f.Map)(),g=()=>e=>{const t=e.getSystem().specSelectors.specJson();return(0,d.isSwagger2)(t)},y=()=>e=>{const t=e.getSystem().specSelectors.specJson();return(0,d.isOAS30)(t)},v=()=>e=>e.getSystem().specSelectors.isOAS30();function b(e){return function(t){for(var n=arguments.length,r=new Array(n>1?n-1:0),o=1;o{if(n.specSelectors.isOAS3()){const o=e(t,...r);return"function"==typeof o?o(n):o}return null}}}const w=b((()=>e=>e.specSelectors.specJson().get("servers",m))),E=b(((e,t)=>{let{callbacks:n,specPath:r}=t;return e=>{var t;const s=e.specSelectors.validOperationMethods();return f.Map.isMap(n)?o()(t=i()(n).call(n,((e,t,n)=>f.Map.isMap(t)?i()(t).call(t,((e,t,i)=>{var a,c;if(!f.Map.isMap(t))return e;const p=o()(a=l()(c=t.entrySeq()).call(c,(e=>{let[t]=e;return u()(s).call(s,t)}))).call(a,(e=>{let[t,o]=e;return{operation:(0,f.Map)({operation:o}),method:t,path:i,callbackName:n,specPath:h()(r).call(r,[n,i,t])}}));return h()(e).call(e,p)}),(0,f.List)()):e),(0,f.List)()).groupBy((e=>e.callbackName))).call(t,(e=>e.toArray())).toObject():{}}}))},92044:(e,t,n)=>{"use strict";n.r(t),n.d(t,{basePath:()=>d,consumes:()=>m,definitions:()=>c,hasHost:()=>u,host:()=>f,produces:()=>g,schemes:()=>y,securityDefinitions:()=>p,validOperationMethods:()=>h});var r=n(20573),o=n(33881),s=n(43393);const i=(0,s.Map)();function a(e){return(t,n)=>function(){if(n.getSystem().specSelectors.isOAS3()){const t=e(...arguments);return"function"==typeof t?t(n):t}return t(...arguments)}}const l=a((0,r.P1)((()=>null))),c=a((()=>e=>{const t=e.getSystem().specSelectors.specJson().getIn(["components","schemas"]);return s.Map.isMap(t)?t:i})),u=a((()=>e=>e.getSystem().specSelectors.specJson().hasIn(["servers",0]))),p=a((0,r.P1)(o.specJsonWithResolvedSubtrees,(e=>e.getIn(["components","securitySchemes"])||null))),h=(e,t)=>function(n){if(t.specSelectors.isOAS3())return t.oas3Selectors.validOperationMethods();for(var r=arguments.length,o=new Array(r>1?r-1:0),s=1;s{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);const o=(0,n(7779).OAS3ComponentWrapFactory)((e=>{let{Ori:t,...n}=e;const{schema:o,getComponent:s,errSelectors:i,authorized:a,onAuthChange:l,name:c}=n,u=s("HttpAuth");return"http"===o.get("type")?r.createElement(u,{key:c,schema:o,name:c,errSelectors:i,authorized:a,getComponent:s,onChange:l}):r.createElement(t,n)}))},37761:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>c});var r=n(22460),o=n(70356),s=n(69487),i=n(50058),a=n(53499),l=n(90287);const c={Markdown:r.default,AuthItem:o.default,JsonSchema_string:l.default,VersionStamp:s.default,model:a.default,onlineValidatorBadge:i.default}},90287:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);const o=(0,n(7779).OAS3ComponentWrapFactory)((e=>{let{Ori:t,...n}=e;const{schema:o,getComponent:s,errors:i,onChange:a}=n,l=o&&o.get?o.get("format"):null,c=o&&o.get?o.get("type"):null,u=s("Input");return c&&"string"===c&&l&&("binary"===l||"base64"===l)?r.createElement(u,{type:"file",className:i.length?"invalid":"",title:i.length?i:"",onChange:e=>{a(e.target.files[0])},disabled:t.isDisabled}):r.createElement(t,n)}))},22460:(e,t,n)=>{"use strict";n.r(t),n.d(t,{Markdown:()=>h,default:()=>f});var r=n(81607),o=n.n(r),s=n(67294),i=n(94184),a=n.n(i),l=n(89927),c=n(7779),u=n(4599);const p=new l._("commonmark");p.block.ruler.enable(["table"]),p.set({linkTarget:"_blank"});const h=e=>{let{source:t,className:n="",getConfigs:r}=e;if("string"!=typeof t)return null;if(t){const{useUnsafeMarkdown:e}=r(),i=p.render(t),l=(0,u.s)(i,{useUnsafeMarkdown:e});let c;return"string"==typeof l&&(c=o()(l).call(l)),s.createElement("div",{dangerouslySetInnerHTML:{__html:c},className:a()(n,"renderedMarkdown")})}return null};h.defaultProps={getConfigs:()=>({useUnsafeMarkdown:!1})};const f=(0,c.OAS3ComponentWrapFactory)(h)},53499:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>c});var r=n(23101),o=n.n(r),s=n(67294),i=n(7779),a=n(53795);class l extends s.Component{render(){let{getConfigs:e,schema:t}=this.props,n=["model-box"],r=null;return!0===t.get("deprecated")&&(n.push("deprecated"),r=s.createElement("span",{className:"model-deprecated-warning"},"Deprecated:")),s.createElement("div",{className:n.join(" ")},r,s.createElement(a.Z,o()({},this.props,{getConfigs:e,depth:1,expandDepth:this.props.expandDepth||0})))}}const c=(0,i.OAS3ComponentWrapFactory)(l)},50058:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});var r=n(7779),o=n(5623);const s=(0,r.OAS3ComponentWrapFactory)(o.Z)},69487:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);const o=(0,n(7779).OAS30ComponentWrapFactory)((e=>{const{Ori:t}=e;return r.createElement("span",null,r.createElement(t,e),r.createElement("small",{className:"version-stamp"},r.createElement("pre",{className:"version"},"OAS 3.0")))}))},92372:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>a});var r=n(76986),o=n.n(r),s=n(25800),i=n(84380);const a=function(e){let{fn:t,getSystem:n}=e;if(t.jsonSchema202012){const e=(0,s.makeIsExpandable)(t.jsonSchema202012.isExpandable,n);o()(this.fn.jsonSchema202012,{isExpandable:e,getProperties:s.getProperties})}if("function"==typeof t.sampleFromSchema&&t.jsonSchema202012){const e=(0,i.wrapOAS31Fn)({sampleFromSchema:t.jsonSchema202012.sampleFromSchema,sampleFromSchemaGeneric:t.jsonSchema202012.sampleFromSchemaGeneric,createXMLExample:t.jsonSchema202012.createXMLExample,memoizedSampleFromSchema:t.jsonSchema202012.memoizedSampleFromSchema,memoizedCreateXMLExample:t.jsonSchema202012.memoizedCreateXMLExample},n());o()(this.fn,e)}}},89503:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});var r=n(67294),o=n(90242);const s=e=>{let{getComponent:t,specSelectors:n}=e;const s=n.selectContactNameField(),i=n.selectContactUrl(),a=n.selectContactEmailField(),l=t("Link");return r.createElement("div",{className:"info__contact"},i&&r.createElement("div",null,r.createElement(l,{href:(0,o.Nm)(i),target:"_blank"},s," - Website")),a&&r.createElement(l,{href:(0,o.Nm)(`mailto:${a}`)},i?`Send email to ${s}`:`Contact ${s}`))}},16133:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});var r=n(67294),o=n(90242);const s=e=>{let{getComponent:t,specSelectors:n}=e;const s=n.version(),i=n.url(),a=n.basePath(),l=n.host(),c=n.selectInfoSummaryField(),u=n.selectInfoDescriptionField(),p=n.selectInfoTitleField(),h=n.selectInfoTermsOfServiceUrl(),f=n.selectExternalDocsUrl(),d=n.selectExternalDocsDescriptionField(),m=n.contact(),g=n.license(),y=t("Markdown",!0),v=t("Link"),b=t("VersionStamp"),w=t("InfoUrl"),E=t("InfoBasePath"),x=t("License",!0),S=t("Contact",!0),_=t("JsonSchemaDialect",!0);return r.createElement("div",{className:"info"},r.createElement("hgroup",{className:"main"},r.createElement("h2",{className:"title"},p,s&&r.createElement(b,{version:s})),(l||a)&&r.createElement(E,{host:l,basePath:a}),i&&r.createElement(w,{getComponent:t,url:i})),c&&r.createElement("p",{className:"info__summary"},c),r.createElement("div",{className:"info__description description"},r.createElement(y,{source:u})),h&&r.createElement("div",{className:"info__tos"},r.createElement(v,{target:"_blank",href:(0,o.Nm)(h)},"Terms of service")),m.size>0&&r.createElement(S,null),g.size>0&&r.createElement(x,null),f&&r.createElement(v,{className:"info__extdocs",target:"_blank",href:(0,o.Nm)(f)},d||f),r.createElement(_,null))}},92562:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});var r=n(67294),o=n(90242);const s=e=>{let{getComponent:t,specSelectors:n}=e;const s=n.selectJsonSchemaDialectField(),i=n.selectJsonSchemaDialectDefault(),a=t("Link");return r.createElement(r.Fragment,null,s&&s===i&&r.createElement("p",{className:"info__jsonschemadialect"},"JSON Schema dialect:"," ",r.createElement(a,{target:"_blank",href:(0,o.Nm)(s)},s)),s&&s!==i&&r.createElement("div",{className:"error-wrapper"},r.createElement("div",{className:"no-margin"},r.createElement("div",{className:"errors"},r.createElement("div",{className:"errors-wrapper"},r.createElement("h4",{className:"center"},"Warning"),r.createElement("p",{className:"message"},r.createElement("strong",null,"OpenAPI.jsonSchemaDialect")," field contains a value different from the default value of"," ",r.createElement(a,{target:"_blank",href:i},i),". Values different from the default one are currently not supported. Please either omit the field or provide it with the default value."))))))}},51876:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});var r=n(67294),o=n(90242);const s=e=>{let{getComponent:t,specSelectors:n}=e;const s=n.selectLicenseNameField(),i=n.selectLicenseUrl(),a=t("Link");return r.createElement("div",{className:"info__license"},i?r.createElement("div",{className:"info__license__url"},r.createElement(a,{target:"_blank",href:(0,o.Nm)(i)},s)):r.createElement("span",null,s))}},92718:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>l});var r=n(58118),o=n.n(r),s=n(67294);n(23930);const i=e=>"string"==typeof e&&o()(e).call(e,"#/components/schemas/")?(e=>{const t=e.replace(/~1/g,"/").replace(/~0/g,"~");try{return decodeURIComponent(t)}catch{return t}})(e.replace(/^.*#\/components\/schemas\//,"")):null,a=(0,s.forwardRef)(((e,t)=>{let{schema:n,getComponent:r,onToggle:o}=e;const a=r("JSONSchema202012"),l=i(n.get("$$ref")),c=(0,s.useCallback)(((e,t)=>{o(l,t)}),[l,o]);return s.createElement(a,{name:l,schema:n.toJS(),ref:t,onExpand:c})}));a.defaultProps={name:"",displayName:"",isRef:!1,required:!1,expandDepth:0,depth:1,includeReadOnly:!1,includeWriteOnly:!1,onToggle:()=>{}};const l=a},20263:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>h});var r=n(28222),o=n.n(r),s=n(97606),i=n.n(s),a=n(2018),l=n.n(a),c=n(67294),u=n(94184),p=n.n(u);const h=e=>{var t;let{specActions:n,specSelectors:r,layoutSelectors:s,layoutActions:a,getComponent:u,getConfigs:h}=e;const f=r.selectSchemas(),d=o()(f).length>0,m=["components","schemas"],{docExpansion:g,defaultModelsExpandDepth:y}=h(),v=y>0&&"none"!==g,b=s.isShown(m,v),w=u("Collapse"),E=u("JSONSchema202012");(0,c.useEffect)((()=>{const e=b&&y>1,t=null!=r.specResolvedSubtree(m);e&&!t&&n.requestResolvedSubtree(m)}),[b,y]);const x=(0,c.useCallback)((()=>{a.show(m,!b)}),[b]),S=(0,c.useCallback)((e=>{null!==e&&a.readyToScroll(m,e)}),[]),_=e=>t=>{null!==t&&a.readyToScroll([...m,e],t)},j=e=>(t,o)=>{if(o){const t=[...m,e];null!=r.specResolvedSubtree(t)||n.requestResolvedSubtree([...m,e])}};return!d||y<0?null:c.createElement("section",{className:p()("models",{"is-open":b}),ref:S},c.createElement("h4",null,c.createElement("button",{"aria-expanded":b,className:"models-control",onClick:x},c.createElement("span",null,"Schemas"),c.createElement("svg",{width:"20",height:"20","aria-hidden":"true",focusable:"false"},c.createElement("use",{xlinkHref:b?"#large-arrow-up":"#large-arrow-down"})))),c.createElement(w,{isOpened:b},i()(t=l()(f)).call(t,(e=>{let[t,n]=e;return c.createElement(E,{key:t,ref:_(t),schema:n,name:t,onExpand:j(t)})}))))}},33429:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);const o=e=>{let{bypass:t,isSwagger2:n,isOAS3:o,isOAS31:s,alsoShow:i,children:a}=e;return t?r.createElement("div",null,a):n&&(o||s)?r.createElement("div",{className:"version-pragma"},i,r.createElement("div",{className:"version-pragma__message version-pragma__message--ambiguous"},r.createElement("div",null,r.createElement("h3",null,"Unable to render this definition"),r.createElement("p",null,r.createElement("code",null,"swagger")," and ",r.createElement("code",null,"openapi")," fields cannot be present in the same Swagger or OpenAPI definition. Please remove one of the fields."),r.createElement("p",null,"Supported version fields are ",r.createElement("code",null,'swagger: "2.0"')," and those that match ",r.createElement("code",null,"openapi: 3.x.y")," (for example,"," ",r.createElement("code",null,"openapi: 3.1.0"),").")))):n||o||s?r.createElement("div",null,a):r.createElement("div",{className:"version-pragma"},i,r.createElement("div",{className:"version-pragma__message version-pragma__message--missing"},r.createElement("div",null,r.createElement("h3",null,"Unable to render this definition"),r.createElement("p",null,"The provided definition does not specify a valid version field."),r.createElement("p",null,"Please indicate a valid Swagger or OpenAPI version field. Supported version fields are ",r.createElement("code",null,'swagger: "2.0"')," and those that match ",r.createElement("code",null,"openapi: 3.x.y")," (for example,"," ",r.createElement("code",null,"openapi: 3.1.0"),")."))))}},39508:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>l});var r=n(28222),o=n.n(r),s=n(97606),i=n.n(s),a=n(67294);const l=e=>{let{specSelectors:t,getComponent:n}=e;const r=t.selectWebhooksOperations(),s=o()(r),l=n("OperationContainer",!0);return 0===s.length?null:a.createElement("div",{className:"webhooks"},a.createElement("h2",null,"Webhooks"),i()(s).call(s,(e=>{var t;return a.createElement("div",{key:`${e}-webhook`},i()(t=r[e]).call(t,(t=>a.createElement(l,{key:`${e}-${t.method}-webhook`,op:t.operation,tag:"webhooks",method:t.method,path:e,specPath:t.specPath,allowTryItOut:!1}))))})))}},84380:(e,t,n)=>{"use strict";n.r(t),n.d(t,{createOnlyOAS31ComponentWrapper:()=>g,createOnlyOAS31Selector:()=>f,createOnlyOAS31SelectorWrapper:()=>d,createSystemSelector:()=>m,isOAS31:()=>h,wrapOAS31Fn:()=>y});var r=n(23101),o=n.n(r),s=n(82865),i=n.n(s),a=n(97606),l=n.n(a),c=n(2018),u=n.n(c),p=n(67294);const h=e=>{const t=e.get("openapi");return"string"==typeof t&&/^3\.1\.(?:[1-9]\d*|0)$/.test(t)},f=e=>function(t){for(var n=arguments.length,r=new Array(n>1?n-1:0),o=1;o{if(n.getSystem().specSelectors.isOAS31()){const o=e(t,...r);return"function"==typeof o?o(n):o}return null}},d=e=>(t,n)=>function(r){for(var o=arguments.length,s=new Array(o>1?o-1:0),i=1;ifunction(t){for(var n=arguments.length,r=new Array(n>1?n-1:0),o=1;o{const o=e(t,n,...r);return"function"==typeof o?o(n):o}},g=e=>(t,n)=>r=>n.specSelectors.isOAS31()?p.createElement(e,o()({},r,{originalComponent:t,getSystem:n.getSystem})):p.createElement(t,r),y=(e,t)=>{var n;const{fn:r,specSelectors:o}=t;return i()(l()(n=u()(e)).call(n,(e=>{let[t,n]=e;const s=r[t];return[t,function(){return o.isOAS31()?n(...arguments):"function"==typeof s?s(...arguments):void 0}]})))}},29806:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>P});var r=n(39508),o=n(51876),s=n(89503),i=n(16133),a=n(92562),l=n(33429),c=n(92718),u=n(20263),p=n(6608),h=n(77423),f=n(284),d=n(17042),m=n(22914),g=n(41434),y=n(1122),v=n(84380),b=n(9305),w=n(32884),E=n(64280),x=n(59450),S=n(36617),_=n(19525),j=n(25324),O=n(80809),k=n(14951),A=n(77536),C=n(92372);const P=e=>{let{fn:t}=e;const n=t.createSystemSelector||v.createSystemSelector,P=t.createOnlyOAS31Selector||v.createOnlyOAS31Selector;return{afterLoad:C.default,fn:{isOAS31:v.isOAS31,createSystemSelector:v.createSystemSelector,createOnlyOAS31Selector:v.createOnlyOAS31Selector},components:{Webhooks:r.default,JsonSchemaDialect:a.default,OAS31Info:i.default,OAS31License:o.default,OAS31Contact:s.default,OAS31VersionPragmaFilter:l.default,OAS31Model:c.default,OAS31Models:u.default,JSONSchema202012KeywordExample:x.default,JSONSchema202012KeywordXml:S.default,JSONSchema202012KeywordDiscriminator:_.default,JSONSchema202012KeywordExternalDocs:j.default},wrapComponents:{InfoContainer:f.default,License:p.default,Contact:h.default,VersionPragmaFilter:g.default,VersionStamp:y.default,Model:d.default,Models:m.default,JSONSchema202012KeywordDescription:O.default,JSONSchema202012KeywordDefault:k.default,JSONSchema202012KeywordProperties:A.default},statePlugins:{spec:{selectors:{isOAS31:n(b.isOAS31),license:b.license,selectLicenseNameField:b.selectLicenseNameField,selectLicenseUrlField:b.selectLicenseUrlField,selectLicenseIdentifierField:P(b.selectLicenseIdentifierField),selectLicenseUrl:n(b.selectLicenseUrl),contact:b.contact,selectContactNameField:b.selectContactNameField,selectContactEmailField:b.selectContactEmailField,selectContactUrlField:b.selectContactUrlField,selectContactUrl:n(b.selectContactUrl),selectInfoTitleField:b.selectInfoTitleField,selectInfoSummaryField:P(b.selectInfoSummaryField),selectInfoDescriptionField:b.selectInfoDescriptionField,selectInfoTermsOfServiceField:b.selectInfoTermsOfServiceField,selectInfoTermsOfServiceUrl:n(b.selectInfoTermsOfServiceUrl),selectExternalDocsDescriptionField:b.selectExternalDocsDescriptionField,selectExternalDocsUrlField:b.selectExternalDocsUrlField,selectExternalDocsUrl:n(b.selectExternalDocsUrl),webhooks:P(b.webhooks),selectWebhooksOperations:P(n(b.selectWebhooksOperations)),selectJsonSchemaDialectField:b.selectJsonSchemaDialectField,selectJsonSchemaDialectDefault:b.selectJsonSchemaDialectDefault,selectSchemas:n(b.selectSchemas)},wrapSelectors:{isOAS3:w.isOAS3,selectLicenseUrl:w.selectLicenseUrl}},oas31:{selectors:{selectLicenseUrl:P(n(E.selectLicenseUrl))}}}}}},45989:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);const o=e=>{let{schema:t,getSystem:n}=e;if(null==t||!t.description)return null;const{getComponent:o}=n(),s=o("Markdown");return r.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--description"},r.createElement("div",{className:"json-schema-2020-12-core-keyword__value json-schema-2020-12-core-keyword__value--secondary"},r.createElement(s,{source:t.description})))}},19525:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>c});var r=n(28222),o=n.n(r),s=n(67294),i=n(94184),a=n.n(i),l=n(7749);const c=e=>{let{schema:t,getSystem:n}=e;const r=(null==t?void 0:t.discriminator)||{},{fn:i,getComponent:c}=n(),{useIsExpandedDeeply:u,useComponent:p}=i.jsonSchema202012,h=u(),f=!!r.mapping,[d,m]=(0,s.useState)(h),[g,y]=(0,s.useState)(!1),v=p("Accordion"),b=p("ExpandDeepButton"),w=c("JSONSchema202012DeepExpansionContext")(),E=(0,s.useCallback)((()=>{m((e=>!e))}),[]),x=(0,s.useCallback)(((e,t)=>{m(t),y(t)}),[]);return 0===o()(r).length?null:s.createElement(w.Provider,{value:g},s.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--discriminator"},f?s.createElement(s.Fragment,null,s.createElement(v,{expanded:d,onChange:E},s.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary"},"Discriminator")),s.createElement(b,{expanded:d,onClick:x})):s.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary"},"Discriminator"),r.propertyName&&s.createElement("span",{className:"json-schema-2020-12__attribute json-schema-2020-12__attribute--muted"},r.propertyName),s.createElement("strong",{className:"json-schema-2020-12__attribute json-schema-2020-12__attribute--primary"},"object"),s.createElement("ul",{className:a()("json-schema-2020-12-keyword__children",{"json-schema-2020-12-keyword__children--collapsed":!d})},d&&s.createElement("li",{className:"json-schema-2020-12-property"},s.createElement(l.default,{discriminator:r})))))}},7749:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>p});var r=n(28222),o=n.n(r),s=n(97606),i=n.n(s),a=n(2018),l=n.n(a),c=n(67294);const u=e=>{var t;let{discriminator:n}=e;const r=(null==n?void 0:n.mapping)||{};return 0===o()(r).length?null:i()(t=l()(r)).call(t,(e=>{let[t,n]=e;return c.createElement("div",{key:`${t}-${n}`,className:"json-schema-2020-12-keyword"},c.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary"},t),c.createElement("span",{className:"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary"},n))}))};u.defaultProps={mapping:void 0};const p=u},59450:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);const o=e=>{let{schema:t,getSystem:n}=e;const{fn:o}=n(),{hasKeyword:s,stringify:i}=o.jsonSchema202012.useFn();return s(t,"example")?r.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--example"},r.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary"},"Example"),r.createElement("span",{className:"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--const"},i(t.example))):null}},25324:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>c});var r=n(28222),o=n.n(r),s=n(67294),i=n(94184),a=n.n(i),l=n(90242);const c=e=>{let{schema:t,getSystem:n}=e;const r=(null==t?void 0:t.externalDocs)||{},{fn:i,getComponent:c}=n(),{useIsExpandedDeeply:u,useComponent:p}=i.jsonSchema202012,h=u(),f=!(!r.description&&!r.url),[d,m]=(0,s.useState)(h),[g,y]=(0,s.useState)(!1),v=p("Accordion"),b=p("ExpandDeepButton"),w=c("JSONSchema202012KeywordDescription"),E=c("Link"),x=c("JSONSchema202012DeepExpansionContext")(),S=(0,s.useCallback)((()=>{m((e=>!e))}),[]),_=(0,s.useCallback)(((e,t)=>{m(t),y(t)}),[]);return 0===o()(r).length?null:s.createElement(x.Provider,{value:g},s.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--externalDocs"},f?s.createElement(s.Fragment,null,s.createElement(v,{expanded:d,onChange:S},s.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary"},"External documentation")),s.createElement(b,{expanded:d,onClick:_})):s.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary"},"External documentation"),s.createElement("strong",{className:"json-schema-2020-12__attribute json-schema-2020-12__attribute--primary"},"object"),s.createElement("ul",{className:a()("json-schema-2020-12-keyword__children",{"json-schema-2020-12-keyword__children--collapsed":!d})},d&&s.createElement(s.Fragment,null,r.description&&s.createElement("li",{className:"json-schema-2020-12-property"},s.createElement(w,{schema:r,getSystem:n})),r.url&&s.createElement("li",{className:"json-schema-2020-12-property"},s.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword"},s.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary"},"url"),s.createElement("span",{className:"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary"},s.createElement(E,{target:"_blank",href:(0,l.Nm)(r.url)},r.url))))))))}},9023:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>g});var r=n(58309),o=n.n(r),s=n(28222),i=n.n(s),a=n(97606),l=n.n(a),c=n(2018),u=n.n(c),p=n(58118),h=n.n(p),f=n(67294),d=n(94184),m=n.n(d);const g=e=>{var t;let{schema:n,getSystem:r}=e;const{fn:s}=r(),{useComponent:a}=s.jsonSchema202012,{getDependentRequired:c,getProperties:p}=s.jsonSchema202012.useFn(),d=s.jsonSchema202012.useConfig(),g=o()(null==n?void 0:n.required)?n.required:[],y=a("JSONSchema"),v=p(n,d);return 0===i()(v).length?null:f.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--properties"},f.createElement("ul",null,l()(t=u()(v)).call(t,(e=>{let[t,r]=e;const o=h()(g).call(g,t),s=c(t,n);return f.createElement("li",{key:t,className:m()("json-schema-2020-12-property",{"json-schema-2020-12-property--required":o})},f.createElement(y,{name:t,schema:r,dependentRequired:s}))}))))}},36617:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>l});var r=n(28222),o=n.n(r),s=n(67294),i=n(94184),a=n.n(i);const l=e=>{let{schema:t,getSystem:n}=e;const r=(null==t?void 0:t.xml)||{},{fn:i,getComponent:l}=n(),{useIsExpandedDeeply:c,useComponent:u}=i.jsonSchema202012,p=c(),h=!!(r.name||r.namespace||r.prefix),[f,d]=(0,s.useState)(p),[m,g]=(0,s.useState)(!1),y=u("Accordion"),v=u("ExpandDeepButton"),b=l("JSONSchema202012DeepExpansionContext")(),w=(0,s.useCallback)((()=>{d((e=>!e))}),[]),E=(0,s.useCallback)(((e,t)=>{d(t),g(t)}),[]);return 0===o()(r).length?null:s.createElement(b.Provider,{value:m},s.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword--xml"},h?s.createElement(s.Fragment,null,s.createElement(y,{expanded:f,onChange:w},s.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary"},"XML")),s.createElement(v,{expanded:f,onClick:E})):s.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary"},"XML"),!0===r.attribute&&s.createElement("span",{className:"json-schema-2020-12__attribute json-schema-2020-12__attribute--muted"},"attribute"),!0===r.wrapped&&s.createElement("span",{className:"json-schema-2020-12__attribute json-schema-2020-12__attribute--muted"},"wrapped"),s.createElement("strong",{className:"json-schema-2020-12__attribute json-schema-2020-12__attribute--primary"},"object"),s.createElement("ul",{className:a()("json-schema-2020-12-keyword__children",{"json-schema-2020-12-keyword__children--collapsed":!f})},f&&s.createElement(s.Fragment,null,r.name&&s.createElement("li",{className:"json-schema-2020-12-property"},s.createElement("div",{className:"json-schema-2020-12-keyword json-schema-2020-12-keyword"},s.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary"},"name"),s.createElement("span",{className:"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary"},r.name))),r.namespace&&s.createElement("li",{className:"json-schema-2020-12-property"},s.createElement("div",{className:"json-schema-2020-12-keyword"},s.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary"},"namespace"),s.createElement("span",{className:"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary"},r.namespace))),r.prefix&&s.createElement("li",{className:"json-schema-2020-12-property"},s.createElement("div",{className:"json-schema-2020-12-keyword"},s.createElement("span",{className:"json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary"},"prefix"),s.createElement("span",{className:"json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary"},r.prefix)))))))}},25800:(e,t,n)=>{"use strict";n.r(t),n.d(t,{getProperties:()=>u,makeIsExpandable:()=>c});var r=n(2018),o=n.n(r),s=n(14418),i=n.n(s),a=n(82865),l=n.n(a);const c=(e,t)=>{const{fn:n}=t();if("function"!=typeof e)return null;const{hasKeyword:r}=n.jsonSchema202012;return t=>e(t)||r(t,"example")||(null==t?void 0:t.xml)||(null==t?void 0:t.discriminator)||(null==t?void 0:t.externalDocs)},u=(e,t)=>{let{includeReadOnly:n,includeWriteOnly:r}=t;if(null==e||!e.properties)return{};const s=o()(e.properties),a=i()(s).call(s,(e=>{let[,t]=e;const o=!0===(null==t?void 0:t.readOnly),s=!0===(null==t?void 0:t.writeOnly);return(!o||n)&&(!s||r)}));return l()(a)}},14951:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);const o=(0,n(84380).createOnlyOAS31ComponentWrapper)((e=>{let{schema:t,getSystem:n,originalComponent:o}=e;const{getComponent:s}=n(),i=s("JSONSchema202012KeywordDiscriminator"),a=s("JSONSchema202012KeywordXml"),l=s("JSONSchema202012KeywordExample"),c=s("JSONSchema202012KeywordExternalDocs");return r.createElement(r.Fragment,null,r.createElement(o,{schema:t}),r.createElement(i,{schema:t,getSystem:n}),r.createElement(a,{schema:t,getSystem:n}),r.createElement(c,{schema:t,getSystem:n}),r.createElement(l,{schema:t,getSystem:n}))}))},80809:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(45989);const o=(0,n(84380).createOnlyOAS31ComponentWrapper)(r.default)},77536:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(9023);const o=(0,n(84380).createOnlyOAS31ComponentWrapper)(r.default)},64280:(e,t,n)=>{"use strict";n.r(t),n.d(t,{selectLicenseUrl:()=>s});var r=n(20573),o=n(63543);const s=(0,r.P1)(((e,t)=>t.specSelectors.url()),((e,t)=>t.oas3Selectors.selectedServer()),((e,t)=>t.specSelectors.selectLicenseUrlField()),((e,t)=>t.specSelectors.selectLicenseIdentifierField()),((e,t,n,r)=>n?(0,o.mn)(n,e,{selectedServer:t}):r?`https://spdx.org/licenses/${r}.html`:void 0))},9305:(e,t,n)=>{"use strict";n.r(t),n.d(t,{contact:()=>A,isOAS31:()=>w,license:()=>S,selectContactEmailField:()=>P,selectContactNameField:()=>C,selectContactUrl:()=>I,selectContactUrlField:()=>N,selectExternalDocsDescriptionField:()=>L,selectExternalDocsUrl:()=>$,selectExternalDocsUrlField:()=>B,selectInfoDescriptionField:()=>M,selectInfoSummaryField:()=>R,selectInfoTermsOfServiceField:()=>D,selectInfoTermsOfServiceUrl:()=>F,selectInfoTitleField:()=>T,selectJsonSchemaDialectDefault:()=>U,selectJsonSchemaDialectField:()=>q,selectLicenseIdentifierField:()=>k,selectLicenseNameField:()=>_,selectLicenseUrl:()=>O,selectLicenseUrlField:()=>j,selectSchemas:()=>z,selectWebhooksOperations:()=>x,webhooks:()=>E});var r=n(97606),o=n.n(r),s=n(24282),i=n.n(s),a=n(14418),l=n.n(a),c=n(58118),u=n.n(c),p=n(39022),h=n.n(p),f=n(2018),d=n.n(f),m=n(43393),g=n(20573),y=n(63543),v=n(84380);const b=(0,m.Map)(),w=(0,g.P1)(((e,t)=>t.specSelectors.specJson()),v.isOAS31),E=()=>e=>e.specSelectors.specJson().get("webhooks",b),x=(0,g.P1)(((e,t)=>t.specSelectors.webhooks()),((e,t)=>t.specSelectors.validOperationMethods()),((e,t)=>t.specSelectors.specResolvedSubtree(["webhooks"])),((e,t)=>{var n;return m.Map.isMap(e)?o()(n=i()(e).call(e,((e,n,r)=>{var s,i;if(!m.Map.isMap(n))return e;const a=o()(s=l()(i=n.entrySeq()).call(i,(e=>{let[n]=e;return u()(t).call(t,n)}))).call(s,(e=>{let[t,n]=e;return{operation:(0,m.Map)({operation:n}),method:t,path:r,specPath:(0,m.List)(["webhooks",r,t])}}));return h()(e).call(e,a)}),(0,m.List)()).groupBy((e=>e.path))).call(n,(e=>e.toArray())).toObject():{}})),S=()=>e=>e.specSelectors.info().get("license",b),_=()=>e=>e.specSelectors.license().get("name","License"),j=()=>e=>e.specSelectors.license().get("url"),O=(0,g.P1)(((e,t)=>t.specSelectors.url()),((e,t)=>t.oas3Selectors.selectedServer()),((e,t)=>t.specSelectors.selectLicenseUrlField()),((e,t,n)=>{if(n)return(0,y.mn)(n,e,{selectedServer:t})})),k=()=>e=>e.specSelectors.license().get("identifier"),A=()=>e=>e.specSelectors.info().get("contact",b),C=()=>e=>e.specSelectors.contact().get("name","the developer"),P=()=>e=>e.specSelectors.contact().get("email"),N=()=>e=>e.specSelectors.contact().get("url"),I=(0,g.P1)(((e,t)=>t.specSelectors.url()),((e,t)=>t.oas3Selectors.selectedServer()),((e,t)=>t.specSelectors.selectContactUrlField()),((e,t,n)=>{if(n)return(0,y.mn)(n,e,{selectedServer:t})})),T=()=>e=>e.specSelectors.info().get("title"),R=()=>e=>e.specSelectors.info().get("summary"),M=()=>e=>e.specSelectors.info().get("description"),D=()=>e=>e.specSelectors.info().get("termsOfService"),F=(0,g.P1)(((e,t)=>t.specSelectors.url()),((e,t)=>t.oas3Selectors.selectedServer()),((e,t)=>t.specSelectors.selectInfoTermsOfServiceField()),((e,t,n)=>{if(n)return(0,y.mn)(n,e,{selectedServer:t})})),L=()=>e=>e.specSelectors.externalDocs().get("description"),B=()=>e=>e.specSelectors.externalDocs().get("url"),$=(0,g.P1)(((e,t)=>t.specSelectors.url()),((e,t)=>t.oas3Selectors.selectedServer()),((e,t)=>t.specSelectors.selectExternalDocsUrlField()),((e,t,n)=>{if(n)return(0,y.mn)(n,e,{selectedServer:t})})),q=()=>e=>e.specSelectors.specJson().get("jsonSchemaDialect"),U=()=>"https://spec.openapis.org/oas/3.1/dialect/base",z=(0,g.P1)(((e,t)=>t.specSelectors.definitions()),((e,t)=>t.specSelectors.specResolvedSubtree(["components","schemas"])),((e,t)=>{var n;return m.Map.isMap(e)?m.Map.isMap(t)?i()(n=d()(e.toJS())).call(n,((e,n)=>{let[r,o]=n;const s=t.get(r);return e[r]=(null==s?void 0:s.toJS())||o,e}),{}):e.toJS():{}}))},32884:(e,t,n)=>{"use strict";n.r(t),n.d(t,{isOAS3:()=>o,selectLicenseUrl:()=>s});var r=n(84380);const o=(e,t)=>function(n){const r=t.specSelectors.isOAS31();for(var o=arguments.length,s=new Array(o>1?o-1:0),i=1;i(e,t)=>t.oas31Selectors.selectLicenseUrl()))},77423:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);const o=(0,n(84380).createOnlyOAS31ComponentWrapper)((e=>{let{getSystem:t}=e;const n=t().getComponent("OAS31Contact",!0);return r.createElement(n,null)}))},284:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);const o=(0,n(84380).createOnlyOAS31ComponentWrapper)((e=>{let{getSystem:t}=e;const n=t().getComponent("OAS31Info",!0);return r.createElement(n,null)}))},6608:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);const o=(0,n(84380).createOnlyOAS31ComponentWrapper)((e=>{let{getSystem:t}=e;const n=t().getComponent("OAS31License",!0);return r.createElement(n,null)}))},17042:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>i});var r=n(67294),o=n(84380),s=n(25800);const i=(0,o.createOnlyOAS31ComponentWrapper)((e=>{let{getSystem:t,...n}=e;const o=t(),{getComponent:i,fn:a,getConfigs:l}=o,c=l(),u=i("OAS31Model"),p=i("JSONSchema202012"),h=i("JSONSchema202012Keyword$schema"),f=i("JSONSchema202012Keyword$vocabulary"),d=i("JSONSchema202012Keyword$id"),m=i("JSONSchema202012Keyword$anchor"),g=i("JSONSchema202012Keyword$dynamicAnchor"),y=i("JSONSchema202012Keyword$ref"),v=i("JSONSchema202012Keyword$dynamicRef"),b=i("JSONSchema202012Keyword$defs"),w=i("JSONSchema202012Keyword$comment"),E=i("JSONSchema202012KeywordAllOf"),x=i("JSONSchema202012KeywordAnyOf"),S=i("JSONSchema202012KeywordOneOf"),_=i("JSONSchema202012KeywordNot"),j=i("JSONSchema202012KeywordIf"),O=i("JSONSchema202012KeywordThen"),k=i("JSONSchema202012KeywordElse"),A=i("JSONSchema202012KeywordDependentSchemas"),C=i("JSONSchema202012KeywordPrefixItems"),P=i("JSONSchema202012KeywordItems"),N=i("JSONSchema202012KeywordContains"),I=i("JSONSchema202012KeywordProperties"),T=i("JSONSchema202012KeywordPatternProperties"),R=i("JSONSchema202012KeywordAdditionalProperties"),M=i("JSONSchema202012KeywordPropertyNames"),D=i("JSONSchema202012KeywordUnevaluatedItems"),F=i("JSONSchema202012KeywordUnevaluatedProperties"),L=i("JSONSchema202012KeywordType"),B=i("JSONSchema202012KeywordEnum"),$=i("JSONSchema202012KeywordConst"),q=i("JSONSchema202012KeywordConstraint"),U=i("JSONSchema202012KeywordDependentRequired"),z=i("JSONSchema202012KeywordContentSchema"),V=i("JSONSchema202012KeywordTitle"),W=i("JSONSchema202012KeywordDescription"),J=i("JSONSchema202012KeywordDefault"),K=i("JSONSchema202012KeywordDeprecated"),H=i("JSONSchema202012KeywordReadOnly"),G=i("JSONSchema202012KeywordWriteOnly"),Z=i("JSONSchema202012Accordion"),Y=i("JSONSchema202012ExpandDeepButton"),X=i("JSONSchema202012ChevronRightIcon"),Q=i("withJSONSchema202012Context")(u,{config:{default$schema:"https://spec.openapis.org/oas/3.1/dialect/base",defaultExpandedLevels:c.defaultModelExpandDepth,includeReadOnly:Boolean(n.includeReadOnly),includeWriteOnly:Boolean(n.includeWriteOnly)},components:{JSONSchema:p,Keyword$schema:h,Keyword$vocabulary:f,Keyword$id:d,Keyword$anchor:m,Keyword$dynamicAnchor:g,Keyword$ref:y,Keyword$dynamicRef:v,Keyword$defs:b,Keyword$comment:w,KeywordAllOf:E,KeywordAnyOf:x,KeywordOneOf:S,KeywordNot:_,KeywordIf:j,KeywordThen:O,KeywordElse:k,KeywordDependentSchemas:A,KeywordPrefixItems:C,KeywordItems:P,KeywordContains:N,KeywordProperties:I,KeywordPatternProperties:T,KeywordAdditionalProperties:R,KeywordPropertyNames:M,KeywordUnevaluatedItems:D,KeywordUnevaluatedProperties:F,KeywordType:L,KeywordEnum:B,KeywordConst:$,KeywordConstraint:q,KeywordDependentRequired:U,KeywordContentSchema:z,KeywordTitle:V,KeywordDescription:W,KeywordDefault:J,KeywordDeprecated:K,KeywordReadOnly:H,KeywordWriteOnly:G,Accordion:Z,ExpandDeepButton:Y,ChevronRightIcon:X},fn:{upperFirst:a.upperFirst,isExpandable:(0,s.makeIsExpandable)(a.jsonSchema202012.isExpandable,t),getProperties:s.getProperties}});return r.createElement(Q,n)}))},22914:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>s});var r=n(67294);const o=(0,n(84380).createOnlyOAS31ComponentWrapper)((e=>{let{getSystem:t}=e;const{getComponent:n,fn:s,getConfigs:i}=t(),a=i();if(o.ModelsWithJSONSchemaContext)return r.createElement(o.ModelsWithJSONSchemaContext,null);const l=n("OAS31Models",!0),c=n("JSONSchema202012"),u=n("JSONSchema202012Keyword$schema"),p=n("JSONSchema202012Keyword$vocabulary"),h=n("JSONSchema202012Keyword$id"),f=n("JSONSchema202012Keyword$anchor"),d=n("JSONSchema202012Keyword$dynamicAnchor"),m=n("JSONSchema202012Keyword$ref"),g=n("JSONSchema202012Keyword$dynamicRef"),y=n("JSONSchema202012Keyword$defs"),v=n("JSONSchema202012Keyword$comment"),b=n("JSONSchema202012KeywordAllOf"),w=n("JSONSchema202012KeywordAnyOf"),E=n("JSONSchema202012KeywordOneOf"),x=n("JSONSchema202012KeywordNot"),S=n("JSONSchema202012KeywordIf"),_=n("JSONSchema202012KeywordThen"),j=n("JSONSchema202012KeywordElse"),O=n("JSONSchema202012KeywordDependentSchemas"),k=n("JSONSchema202012KeywordPrefixItems"),A=n("JSONSchema202012KeywordItems"),C=n("JSONSchema202012KeywordContains"),P=n("JSONSchema202012KeywordProperties"),N=n("JSONSchema202012KeywordPatternProperties"),I=n("JSONSchema202012KeywordAdditionalProperties"),T=n("JSONSchema202012KeywordPropertyNames"),R=n("JSONSchema202012KeywordUnevaluatedItems"),M=n("JSONSchema202012KeywordUnevaluatedProperties"),D=n("JSONSchema202012KeywordType"),F=n("JSONSchema202012KeywordEnum"),L=n("JSONSchema202012KeywordConst"),B=n("JSONSchema202012KeywordConstraint"),$=n("JSONSchema202012KeywordDependentRequired"),q=n("JSONSchema202012KeywordContentSchema"),U=n("JSONSchema202012KeywordTitle"),z=n("JSONSchema202012KeywordDescription"),V=n("JSONSchema202012KeywordDefault"),W=n("JSONSchema202012KeywordDeprecated"),J=n("JSONSchema202012KeywordReadOnly"),K=n("JSONSchema202012KeywordWriteOnly"),H=n("JSONSchema202012Accordion"),G=n("JSONSchema202012ExpandDeepButton"),Z=n("JSONSchema202012ChevronRightIcon"),Y=n("withJSONSchema202012Context");return o.ModelsWithJSONSchemaContext=Y(l,{config:{default$schema:"https://spec.openapis.org/oas/3.1/dialect/base",defaultExpandedLevels:a.defaultModelsExpandDepth-1,includeReadOnly:!0,includeWriteOnly:!0},components:{JSONSchema:c,Keyword$schema:u,Keyword$vocabulary:p,Keyword$id:h,Keyword$anchor:f,Keyword$dynamicAnchor:d,Keyword$ref:m,Keyword$dynamicRef:g,Keyword$defs:y,Keyword$comment:v,KeywordAllOf:b,KeywordAnyOf:w,KeywordOneOf:E,KeywordNot:x,KeywordIf:S,KeywordThen:_,KeywordElse:j,KeywordDependentSchemas:O,KeywordPrefixItems:k,KeywordItems:A,KeywordContains:C,KeywordProperties:P,KeywordPatternProperties:N,KeywordAdditionalProperties:I,KeywordPropertyNames:T,KeywordUnevaluatedItems:R,KeywordUnevaluatedProperties:M,KeywordType:D,KeywordEnum:F,KeywordConst:L,KeywordConstraint:B,KeywordDependentRequired:$,KeywordContentSchema:q,KeywordTitle:U,KeywordDescription:z,KeywordDefault:V,KeywordDeprecated:W,KeywordReadOnly:J,KeywordWriteOnly:K,Accordion:H,ExpandDeepButton:G,ChevronRightIcon:Z},fn:{upperFirst:s.upperFirst,isExpandable:s.jsonSchema202012.isExpandable,getProperties:s.jsonSchema202012.getProperties}}),r.createElement(o.ModelsWithJSONSchemaContext,null)}));o.ModelsWithJSONSchemaContext=null;const s=o},41434:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>i});var r=n(23101),o=n.n(r),s=n(67294);const i=(e,t)=>e=>{const n=t.specSelectors.isOAS31(),r=t.getComponent("OAS31VersionPragmaFilter");return s.createElement(r,o()({isOAS31:n},e))}},1122:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);const o=(0,n(84380).createOnlyOAS31ComponentWrapper)((e=>{let{originalComponent:t,...n}=e;return r.createElement("span",null,r.createElement(t,n),r.createElement("small",{className:"version-stamp"},r.createElement("pre",{className:"version"},"OAS 3.1")))}))},28560:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>i});var r=n(87198),o=n.n(r);let s=!1;function i(){return{statePlugins:{spec:{wrapActions:{updateSpec:e=>function(){return s=!0,e(...arguments)},updateJsonSpec:(e,t)=>function(){const n=t.getConfigs().onComplete;return s&&"function"==typeof n&&(o()(n,0),s=!1),e(...arguments)}}}}}}},92135:(e,t,n)=>{"use strict";n.r(t),n.d(t,{requestSnippetGenerator_curl_bash:()=>j,requestSnippetGenerator_curl_cmd:()=>O,requestSnippetGenerator_curl_powershell:()=>_});var r=n(11882),o=n.n(r),s=n(81607),i=n.n(s),a=n(35627),l=n.n(a),c=n(97606),u=n.n(c),p=n(12196),h=n.n(p),f=n(74386),d=n.n(f),m=n(58118),g=n.n(m),y=n(27504),v=n(43393);const b=e=>{var t;const n="_**[]";return o()(e).call(e,n)<0?e:i()(t=e.split(n)[0]).call(t)},w=e=>"-d "===e||/^[_\/-]/g.test(e)?e:"'"+e.replace(/'/g,"'\\''")+"'",E=e=>"-d "===(e=e.replace(/\^/g,"^^").replace(/\\"/g,'\\\\"').replace(/"/g,'""').replace(/\n/g,"^\n"))?e.replace(/-d /g,"-d ^\n"):/^[_\/-]/g.test(e)?e:'"'+e+'"',x=e=>"-d "===e?e:/\n/.test(e)?'@"\n'+e.replace(/"/g,'\\"').replace(/`/g,"``").replace(/\$/,"`$")+'\n"@':/^[_\/-]/g.test(e)?e:"'"+e.replace(/"/g,'""').replace(/'/g,"''")+"'";const S=function(e,t,n){let r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"",o=!1,s="";const i=function(){for(var e=arguments.length,n=new Array(e),r=0;rs+=` ${n}`,p=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1;return s+=h()(" ").call(" ",e)};let f=e.get("headers");if(s+="curl"+r,e.has("curlOptions")&&i(...e.get("curlOptions")),i("-X",e.get("method")),c(),p(),a(`${e.get("url")}`),f&&f.size)for(let t of d()(m=e.get("headers")).call(m)){var m;c(),p();let[e,n]=t;a("-H",`${e}: ${n}`),o=o||/^content-type$/i.test(e)&&/^multipart\/form-data$/i.test(n)}const w=e.get("body");var E;if(w)if(o&&g()(E=["POST","PUT","PATCH"]).call(E,e.get("method")))for(let[e,t]of w.entrySeq()){let n=b(e);c(),p(),a("-F"),t instanceof y.Z.File?i(`${n}=@${t.name}${t.type?`;type=${t.type}`:""}`):i(`${n}=${t}`)}else if(w instanceof y.Z.File)c(),p(),a(`--data-binary '@${w.name}'`);else{c(),p(),a("-d ");let t=w;v.Map.isMap(t)?a(function(e){let t=[];for(let[n,r]of e.get("body").entrySeq()){let e=b(n);r instanceof y.Z.File?t.push(` "${e}": {\n "name": "${r.name}"${r.type?`,\n "type": "${r.type}"`:""}\n }`):t.push(` "${e}": ${l()(r,null,2).replace(/(\r\n|\r|\n)/g,"\n ")}`)}return`{\n${t.join(",\n")}\n}`}(e)):("string"!=typeof t&&(t=l()(t)),a(t))}else w||"POST"!==e.get("method")||(c(),p(),a("-d ''"));return s},_=e=>S(e,x,"`\n",".exe"),j=e=>S(e,w,"\\\n"),O=e=>S(e,E,"^\n")},86575:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>i});var r=n(92135),o=n(4669),s=n(84206);const i=()=>({components:{RequestSnippets:s.default},fn:r,statePlugins:{requestSnippets:{selectors:o}}})},84206:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>w});var r=n(14418),o=n.n(r),s=n(25110),i=n.n(s),a=n(86),l=n.n(a),c=n(97606),u=n.n(c),p=n(67294),h=n(27361),f=n.n(h),d=n(23560),m=n.n(d),g=n(74855),y=n(33424);const v={cursor:"pointer",lineHeight:1,display:"inline-flex",backgroundColor:"rgb(250, 250, 250)",paddingBottom:"0",paddingTop:"0",border:"1px solid rgb(51, 51, 51)",borderRadius:"4px 4px 0 0",boxShadow:"none",borderBottom:"none"},b={cursor:"pointer",lineHeight:1,display:"inline-flex",backgroundColor:"rgb(51, 51, 51)",boxShadow:"none",border:"1px solid rgb(51, 51, 51)",paddingBottom:"0",paddingTop:"0",borderRadius:"4px 4px 0 0",marginTop:"-5px",marginRight:"-5px",marginLeft:"-5px",zIndex:"9999",borderBottom:"none"},w=e=>{var t,n;let{request:r,requestSnippetsSelectors:s,getConfigs:a}=e;const c=m()(a)?a():null,h=!1!==f()(c,"syntaxHighlight")&&f()(c,"syntaxHighlight.activated",!0),d=(0,p.useRef)(null),[w,E]=(0,p.useState)(null===(t=s.getSnippetGenerators())||void 0===t?void 0:t.keySeq().first()),[x,S]=(0,p.useState)(null==s?void 0:s.getDefaultExpanded());(0,p.useEffect)((()=>{}),[]),(0,p.useEffect)((()=>{var e;const t=o()(e=i()(d.current.childNodes)).call(e,(e=>{var t;return!!e.nodeType&&(null===(t=e.classList)||void 0===t?void 0:t.contains("curl-command"))}));return l()(t).call(t,(e=>e.addEventListener("mousewheel",C,{passive:!1}))),()=>{l()(t).call(t,(e=>e.removeEventListener("mousewheel",C)))}}),[r]);const _=s.getSnippetGenerators(),j=_.get(w),O=j.get("fn")(r),k=()=>{S(!x)},A=e=>e===w?b:v,C=e=>{const{target:t,deltaY:n}=e,{scrollHeight:r,offsetHeight:o,scrollTop:s}=t;r>o&&(0===s&&n<0||o+s>=r&&n>0)&&e.preventDefault()},P=h?p.createElement(y.d3,{language:j.get("syntax"),className:"curl microlight",style:(0,y.C2)(f()(c,"syntaxHighlight.theme"))},O):p.createElement("textarea",{readOnly:!0,className:"curl",value:O});return p.createElement("div",{className:"request-snippets",ref:d},p.createElement("div",{style:{width:"100%",display:"flex",justifyContent:"flex-start",alignItems:"center",marginBottom:"15px"}},p.createElement("h4",{onClick:()=>k(),style:{cursor:"pointer"}},"Snippets"),p.createElement("button",{onClick:()=>k(),style:{border:"none",background:"none"},title:x?"Collapse operation":"Expand operation"},p.createElement("svg",{className:"arrow",width:"10",height:"10"},p.createElement("use",{href:x?"#large-arrow-down":"#large-arrow",xlinkHref:x?"#large-arrow-down":"#large-arrow"})))),x&&p.createElement("div",{className:"curl-command"},p.createElement("div",{style:{paddingLeft:"15px",paddingRight:"10px",width:"100%",display:"flex"}},u()(n=_.entrySeq()).call(n,(e=>{let[t,n]=e;return p.createElement("div",{style:A(t),className:"btn",key:t,onClick:()=>(e=>{w!==e&&E(e)})(t)},p.createElement("h4",{style:t===w?{color:"white"}:{}},n.get("title")))}))),p.createElement("div",{className:"copy-to-clipboard"},p.createElement(g.CopyToClipboard,{text:O},p.createElement("button",null))),p.createElement("div",null,P)))}},4669:(e,t,n)=>{"use strict";n.r(t),n.d(t,{getActiveLanguage:()=>d,getDefaultExpanded:()=>m,getGenerators:()=>h,getSnippetGenerators:()=>f});var r=n(14418),o=n.n(r),s=n(58118),i=n.n(s),a=n(97606),l=n.n(a),c=n(20573),u=n(43393);const p=e=>e||(0,u.Map)(),h=(0,c.P1)(p,(e=>{const t=e.get("languages"),n=e.get("generators",(0,u.Map)());return!t||t.isEmpty()?n:o()(n).call(n,((e,n)=>i()(t).call(t,n)))})),f=e=>t=>{var n,r;let{fn:s}=t;return o()(n=l()(r=h(e)).call(r,((e,t)=>{const n=(e=>s[`requestSnippetGenerator_${e}`])(t);return"function"!=typeof n?null:e.set("fn",n)}))).call(n,(e=>e))},d=(0,c.P1)(p,(e=>e.get("activeLanguage"))),m=(0,c.P1)(p,(e=>e.get("defaultExpanded")))},36195:(e,t,n)=>{"use strict";n.r(t),n.d(t,{ErrorBoundary:()=>i,default:()=>a});var r=n(67294),o=n(56189),s=n(29403);class i extends r.Component{static getDerivedStateFromError(e){return{hasError:!0,error:e}}constructor(){super(...arguments),this.state={hasError:!1,error:null}}componentDidCatch(e,t){this.props.fn.componentDidCatch(e,t)}render(){const{getComponent:e,targetName:t,children:n}=this.props;if(this.state.hasError){const n=e("Fallback");return r.createElement(n,{name:t})}return n}}i.defaultProps={targetName:"this component",getComponent:()=>s.default,fn:{componentDidCatch:o.componentDidCatch},children:null};const a=i},29403:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(67294);const o=e=>{let{name:t}=e;return r.createElement("div",{className:"fallback"},"😱 ",r.createElement("i",null,"Could not render ","t"===t?"this component":t,", see the console."))}},56189:(e,t,n)=>{"use strict";n.r(t),n.d(t,{componentDidCatch:()=>i,withErrorBoundary:()=>a});var r=n(23101),o=n.n(r),s=n(67294);const i=console.error,a=e=>t=>{const{getComponent:n,fn:r}=e(),i=n("ErrorBoundary"),a=r.getDisplayName(t);class l extends s.Component{render(){return s.createElement(i,{targetName:a,getComponent:n,fn:r},s.createElement(t,o()({},this.props,this.context)))}}var c;return l.displayName=`WithErrorBoundary(${a})`,(c=t).prototype&&c.prototype.isReactComponent&&(l.prototype.mapStateToProps=t.prototype.mapStateToProps),l}},27621:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>u});var r=n(47475),o=n.n(r),s=n(7287),i=n.n(s),a=n(36195),l=n(29403),c=n(56189);const u=function(){let{componentList:e=[],fullOverride:t=!1}=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return n=>{var r;let{getSystem:s}=n;const u=t?e:["App","BaseLayout","VersionPragmaFilter","InfoContainer","ServersContainer","SchemesContainer","AuthorizeBtnContainer","FilterContainer","Operations","OperationContainer","parameters","responses","OperationServers","Models","ModelWrapper",...e],p=i()(u,o()(r=Array(u.length)).call(r,((e,t)=>{let{fn:n}=t;return n.withErrorBoundary(e)})));return{fn:{componentDidCatch:c.componentDidCatch,withErrorBoundary:(0,c.withErrorBoundary)(s)},components:{ErrorBoundary:a.default,Fallback:l.default},wrapComponents:p}}}},72846:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>p});var r=n(24282),o=n.n(r),s=n(35627),i=n.n(s),a=n(59704),l=n.n(a);const c=[{when:/json/,shouldStringifyTypes:["string"]}],u=["object"],p=e=>(t,n,r,s)=>{const{fn:a}=e(),p=a.memoizedSampleFromSchema(t,n,s),h=typeof p,f=o()(c).call(c,((e,t)=>t.when.test(r)?[...e,...t.shouldStringifyTypes]:e),u);return l()(f,(e=>e===h))?i()(p,null,2):p}},16132:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=e=>function(t){var n,r;let o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:void 0;const{fn:a}=e();return"function"==typeof(null===(n=t)||void 0===n?void 0:n.toJS)&&(t=t.toJS()),"function"==typeof(null===(r=i)||void 0===r?void 0:r.toJS)&&(i=i.toJS()),/xml/.test(o)?a.getXmlSampleSchema(t,s,i):/(yaml|yml)/.test(o)?a.getYamlSampleSchema(t,s,o,i):a.getJsonSampleSchema(t,s,o,i)}},81169:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r=e=>(t,n,r)=>{const{fn:o}=e();if(t&&!t.xml&&(t.xml={}),t&&!t.xml.name){if(!t.$$ref&&(t.type||t.items||t.properties||t.additionalProperties))return'\n\x3c!-- XML example cannot be generated; root element name is undefined --\x3e';if(t.$$ref){let e=t.$$ref.match(/\S*\/(\S+)$/);t.xml.name=e[1]}}return o.memoizedCreateXMLExample(t,n,r)}},79431:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>i});var r=n(24278),o=n.n(r),s=n(1272);const i=e=>(t,n,r,i)=>{const{fn:a}=e(),l=a.getJsonSampleSchema(t,n,r,i);let c;try{c=s.ZP.dump(s.ZP.load(l),{lineWidth:-1},{schema:s.A8}),"\n"===c[c.length-1]&&(c=o()(c).call(c,0,c.length-1))}catch(e){return console.error(e),"error: could not generate yaml example"}return c.replace(/\t/g," ")}},29812:(e,t,n)=>{"use strict";n.r(t),n.d(t,{createXMLExample:()=>q,inferSchema:()=>$,memoizedCreateXMLExample:()=>V,memoizedSampleFromSchema:()=>W,sampleFromSchema:()=>U,sampleFromSchemaGeneric:()=>B});var r=n(11882),o=n.n(r),s=n(86),i=n.n(s),a=n(58309),l=n.n(a),c=n(58118),u=n.n(c),p=n(92039),h=n.n(p),f=n(24278),d=n.n(f),m=n(51679),g=n.n(m),y=n(39022),v=n.n(y),b=n(97606),w=n.n(b),E=n(35627),x=n.n(E),S=n(53479),_=n.n(S),j=n(14419),O=n.n(j),k=n(41609),A=n.n(k),C=n(90242),P=n(60314);const N={string:e=>e.pattern?(e=>{try{return new(O())(e).gen()}catch(e){return"string"}})(e.pattern):"string",string_email:()=>"user@example.com","string_date-time":()=>(new Date).toISOString(),string_date:()=>(new Date).toISOString().substring(0,10),string_uuid:()=>"3fa85f64-5717-4562-b3fc-2c963f66afa6",string_hostname:()=>"example.com",string_ipv4:()=>"198.51.100.42",string_ipv6:()=>"2001:0db8:5b96:0000:0000:426f:8e17:642a",number:()=>0,number_float:()=>0,integer:()=>0,boolean:e=>"boolean"!=typeof e.default||e.default},I=e=>{e=(0,C.mz)(e);let{type:t,format:n}=e,r=N[`${t}_${n}`]||N[t];return(0,C.Wl)(r)?r(e):"Unknown Type: "+e.type},T=e=>(0,C.XV)(e,"$$ref",(e=>"string"==typeof e&&o()(e).call(e,"#")>-1)),R=["maxProperties","minProperties"],M=["minItems","maxItems"],D=["minimum","maximum","exclusiveMinimum","exclusiveMaximum"],F=["minLength","maxLength"],L=function(e,t){var n;let r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};var s;(i()(n=["example","default","enum","xml","type",...R,...M,...D,...F]).call(n,(n=>(n=>{void 0===t[n]&&void 0!==e[n]&&(t[n]=e[n])})(n))),void 0!==e.required&&l()(e.required))&&(void 0!==t.required&&t.required.length||(t.required=[]),i()(s=e.required).call(s,(e=>{var n;u()(n=t.required).call(n,e)||t.required.push(e)})));if(e.properties){t.properties||(t.properties={});let n=(0,C.mz)(e.properties);for(let s in n){var a;if(Object.prototype.hasOwnProperty.call(n,s))if(!n[s]||!n[s].deprecated)if(!n[s]||!n[s].readOnly||r.includeReadOnly)if(!n[s]||!n[s].writeOnly||r.includeWriteOnly)if(!t.properties[s])t.properties[s]=n[s],!e.required&&l()(e.required)&&-1!==o()(a=e.required).call(a,s)&&(t.required?t.required.push(s):t.required=[s])}}return e.items&&(t.items||(t.items={}),t.items=L(e.items,t.items,r)),t},B=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:void 0,r=arguments.length>3&&void 0!==arguments[3]&&arguments[3];e&&(0,C.Wl)(e.toJS)&&(e=e.toJS());let s=void 0!==n||e&&void 0!==e.example||e&&void 0!==e.default;const a=!s&&e&&e.oneOf&&e.oneOf.length>0,c=!s&&e&&e.anyOf&&e.anyOf.length>0;if(!s&&(a||c)){const n=(0,C.mz)(a?e.oneOf[0]:e.anyOf[0]);if(L(n,e,t),!e.xml&&n.xml&&(e.xml=n.xml),void 0!==e.example&&void 0!==n.example)s=!0;else if(n.properties){e.properties||(e.properties={});let r=(0,C.mz)(n.properties);for(let s in r){var p;if(Object.prototype.hasOwnProperty.call(r,s))if(!r[s]||!r[s].deprecated)if(!r[s]||!r[s].readOnly||t.includeReadOnly)if(!r[s]||!r[s].writeOnly||t.includeWriteOnly)if(!e.properties[s])e.properties[s]=r[s],!n.required&&l()(n.required)&&-1!==o()(p=n.required).call(p,s)&&(e.required?e.required.push(s):e.required=[s])}}}const f={};let{xml:m,type:y,example:b,properties:E,additionalProperties:x,items:S}=e||{},{includeReadOnly:_,includeWriteOnly:j}=t;m=m||{};let O,{name:k,prefix:P,namespace:N}=m,F={};if(r&&(k=k||"notagname",O=(P?P+":":"")+k,N)){f[P?"xmlns:"+P:"xmlns"]=N}r&&(F[O]=[]);const $=t=>h()(t).call(t,(t=>Object.prototype.hasOwnProperty.call(e,t)));e&&!y&&(E||x||$(R)?y="object":S||$(M)?y="array":$(D)?(y="number",e.type="number"):s||e.enum||(y="string",e.type="string"));const q=t=>{var n,r,o,s,i;null!==(null===(n=e)||void 0===n?void 0:n.maxItems)&&void 0!==(null===(r=e)||void 0===r?void 0:r.maxItems)&&(t=d()(t).call(t,0,null===(i=e)||void 0===i?void 0:i.maxItems));if(null!==(null===(o=e)||void 0===o?void 0:o.minItems)&&void 0!==(null===(s=e)||void 0===s?void 0:s.minItems)){let n=0;for(;t.length<(null===(a=e)||void 0===a?void 0:a.minItems);){var a;t.push(t[n++%t.length])}}return t},U=(0,C.mz)(E);let z,V=0;const W=()=>e&&null!==e.maxProperties&&void 0!==e.maxProperties&&V>=e.maxProperties,J=t=>!e||null===e.maxProperties||void 0===e.maxProperties||!W()&&(!(t=>{var n;return!(e&&e.required&&e.required.length&&u()(n=e.required).call(n,t))})(t)||e.maxProperties-V-(()=>{if(!e||!e.required)return 0;let t=0;var n,o;return r?i()(n=e.required).call(n,(e=>t+=void 0===F[e]?0:1)):i()(o=e.required).call(o,(e=>{var n;return t+=void 0===(null===(n=F[O])||void 0===n?void 0:g()(n).call(n,(t=>void 0!==t[e])))?0:1})),e.required.length-t})()>0);if(z=r?function(n){let o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:void 0;if(e&&U[n]){if(U[n].xml=U[n].xml||{},U[n].xml.attribute){const e=l()(U[n].enum)?U[n].enum[0]:void 0,t=U[n].example,r=U[n].default;return void(f[U[n].xml.name||n]=void 0!==t?t:void 0!==r?r:void 0!==e?e:I(U[n]))}U[n].xml.name=U[n].xml.name||n}else U[n]||!1===x||(U[n]={xml:{name:n}});let s=B(e&&U[n]||void 0,t,o,r);var i;J(n)&&(V++,l()(s)?F[O]=v()(i=F[O]).call(i,s):F[O].push(s))}:(n,o)=>{if(J(n)){if(Object.prototype.hasOwnProperty.call(e,"discriminator")&&e.discriminator&&Object.prototype.hasOwnProperty.call(e.discriminator,"mapping")&&e.discriminator.mapping&&Object.prototype.hasOwnProperty.call(e,"$$ref")&&e.$$ref&&e.discriminator.propertyName===n){for(let t in e.discriminator.mapping)if(-1!==e.$$ref.search(e.discriminator.mapping[t])){F[n]=t;break}}else F[n]=B(U[n],t,o,r);V++}},s){let o;if(o=T(void 0!==n?n:void 0!==b?b:e.default),!r){if("number"==typeof o&&"string"===y)return`${o}`;if("string"!=typeof o||"string"===y)return o;try{return JSON.parse(o)}catch(e){return o}}if(e||(y=l()(o)?"array":typeof o),"array"===y){if(!l()(o)){if("string"==typeof o)return o;o=[o]}const n=e?e.items:void 0;n&&(n.xml=n.xml||m||{},n.xml.name=n.xml.name||m.name);let s=w()(o).call(o,(e=>B(n,t,e,r)));return s=q(s),m.wrapped?(F[O]=s,A()(f)||F[O].push({_attr:f})):F=s,F}if("object"===y){if("string"==typeof o)return o;for(let t in o)Object.prototype.hasOwnProperty.call(o,t)&&(e&&U[t]&&U[t].readOnly&&!_||e&&U[t]&&U[t].writeOnly&&!j||(e&&U[t]&&U[t].xml&&U[t].xml.attribute?f[U[t].xml.name||t]=o[t]:z(t,o[t])));return A()(f)||F[O].push({_attr:f}),F}return F[O]=A()(f)?o:[{_attr:f},o],F}if("object"===y){for(let e in U)Object.prototype.hasOwnProperty.call(U,e)&&(U[e]&&U[e].deprecated||U[e]&&U[e].readOnly&&!_||U[e]&&U[e].writeOnly&&!j||z(e));if(r&&f&&F[O].push({_attr:f}),W())return F;if(!0===x)r?F[O].push({additionalProp:"Anything can be here"}):F.additionalProp1={},V++;else if(x){const n=(0,C.mz)(x),o=B(n,t,void 0,r);if(r&&n.xml&&n.xml.name&&"notagname"!==n.xml.name)F[O].push(o);else{const t=null!==e.minProperties&&void 0!==e.minProperties&&VB(L(S,e,t),t,void 0,r)));else if(l()(S.oneOf)){var G;n=w()(G=S.oneOf).call(G,(e=>B(L(S,e,t),t,void 0,r)))}else{if(!(!r||r&&m.wrapped))return B(S,t,void 0,r);n=[B(S,t,void 0,r)]}return n=q(n),r&&m.wrapped?(F[O]=n,A()(f)||F[O].push({_attr:f}),F):n}let Z;if(e&&l()(e.enum))Z=(0,C.AF)(e.enum)[0];else{if(!e)return;if(Z=I(e),"number"==typeof Z){let t=e.minimum;null!=t&&(e.exclusiveMinimum&&t++,Z=t);let n=e.maximum;null!=n&&(e.exclusiveMaximum&&n--,Z=n)}if("string"==typeof Z&&(null!==e.maxLength&&void 0!==e.maxLength&&(Z=d()(Z).call(Z,0,e.maxLength)),null!==e.minLength&&void 0!==e.minLength)){let t=0;for(;Z.length(e.schema&&(e=e.schema),e.properties&&(e.type="object"),e),q=(e,t,n)=>{const r=B(e,t,n,!0);if(r)return"string"==typeof r?r:_()(r,{declaration:!0,indent:"\t"})},U=(e,t,n)=>B(e,t,n,!1),z=(e,t,n)=>[e,x()(t),x()(n)],V=(0,P.Z)(q,z),W=(0,P.Z)(U,z)},8883:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>l});var r=n(29812),o=n(72846),s=n(79431),i=n(81169),a=n(16132);const l=e=>{let{getSystem:t}=e;return{fn:{inferSchema:r.inferSchema,sampleFromSchema:r.sampleFromSchema,sampleFromSchemaGeneric:r.sampleFromSchemaGeneric,createXMLExample:r.createXMLExample,memoizedSampleFromSchema:r.memoizedSampleFromSchema,memoizedCreateXMLExample:r.memoizedCreateXMLExample,getJsonSampleSchema:(0,o.default)(t),getYamlSampleSchema:(0,s.default)(t),getXmlSampleSchema:(0,i.default)(t),getSampleSchema:(0,a.default)(t)}}}},51228:(e,t,n)=>{"use strict";n.r(t),n.d(t,{CLEAR_REQUEST:()=>ee,CLEAR_RESPONSE:()=>Q,CLEAR_VALIDATE_PARAMS:()=>te,LOG_REQUEST:()=>X,SET_MUTATED_REQUEST:()=>Y,SET_REQUEST:()=>Z,SET_RESPONSE:()=>G,SET_SCHEME:()=>se,UPDATE_EMPTY_PARAM_INCLUSION:()=>K,UPDATE_JSON:()=>W,UPDATE_OPERATION_META_VALUE:()=>ne,UPDATE_PARAM:()=>J,UPDATE_RESOLVED:()=>re,UPDATE_RESOLVED_SUBTREE:()=>oe,UPDATE_SPEC:()=>z,UPDATE_URL:()=>V,VALIDATE_PARAMS:()=>H,changeConsumesValue:()=>_e,changeParam:()=>ye,changeParamByIdentity:()=>ve,changeProducesValue:()=>je,clearRequest:()=>Te,clearResponse:()=>Ie,clearValidateParams:()=>Se,execute:()=>Ne,executeRequest:()=>Pe,invalidateResolvedSubtreeCache:()=>we,logRequest:()=>Ce,parseToJson:()=>pe,requestResolvedSubtree:()=>ge,resolveSpec:()=>fe,setMutatedRequest:()=>Ae,setRequest:()=>ke,setResponse:()=>Oe,setScheme:()=>Re,updateEmptyParamInclusion:()=>xe,updateJsonSpec:()=>ue,updateResolved:()=>le,updateResolvedSubtree:()=>be,updateSpec:()=>ae,updateUrl:()=>ce,validateParams:()=>Ee});var r=n(58309),o=n.n(r),s=n(97606),i=n.n(s),a=n(96718),l=n.n(a),c=n(24282),u=n.n(c),p=n(2250),h=n.n(p),f=n(6226),d=n.n(f),m=n(14418),g=n.n(m),y=n(3665),v=n.n(y),b=n(11882),w=n.n(b),E=n(86),x=n.n(E),S=n(28222),_=n.n(S),j=n(76986),O=n.n(j),k=n(70586),A=n.n(k),C=n(1272),P=n(43393),N=n(84564),I=n.n(N),T=n(7710),R=n(47037),M=n.n(R),D=n(23279),F=n.n(D),L=n(36968),B=n.n(L),$=n(72700),q=n.n($),U=n(90242);const z="spec_update_spec",V="spec_update_url",W="spec_update_json",J="spec_update_param",K="spec_update_empty_param_inclusion",H="spec_validate_param",G="spec_set_response",Z="spec_set_request",Y="spec_set_mutated_request",X="spec_log_request",Q="spec_clear_response",ee="spec_clear_request",te="spec_clear_validate_param",ne="spec_update_operation_meta_value",re="spec_update_resolved",oe="spec_update_resolved_subtree",se="set_scheme",ie=e=>M()(e)?e:"";function ae(e){const t=ie(e).replace(/\t/g," ");if("string"==typeof e)return{type:z,payload:t}}function le(e){return{type:re,payload:e}}function ce(e){return{type:V,payload:e}}function ue(e){return{type:W,payload:e}}const pe=e=>t=>{let{specActions:n,specSelectors:r,errActions:o}=t,{specStr:s}=r,i=null;try{e=e||s(),o.clear({source:"parser"}),i=C.ZP.load(e,{schema:C.A8})}catch(e){return console.error(e),o.newSpecErr({source:"parser",level:"error",message:e.reason,line:e.mark&&e.mark.line?e.mark.line+1:void 0})}return i&&"object"==typeof i?n.updateJsonSpec(i):{}};let he=!1;const fe=(e,t)=>n=>{let{specActions:r,specSelectors:s,errActions:a,fn:{fetch:c,resolve:u,AST:p={}},getConfigs:h}=n;he||(console.warn("specActions.resolveSpec is deprecated since v3.10.0 and will be removed in v4.0.0; use requestResolvedSubtree instead!"),he=!0);const{modelPropertyMacro:f,parameterMacro:d,requestInterceptor:m,responseInterceptor:g}=h();void 0===e&&(e=s.specJson()),void 0===t&&(t=s.url());let y=p.getLineNumberForPath?p.getLineNumberForPath:()=>{},v=s.specStr();return u({fetch:c,spec:e,baseDoc:t,modelPropertyMacro:f,parameterMacro:d,requestInterceptor:m,responseInterceptor:g}).then((e=>{let{spec:t,errors:n}=e;if(a.clear({type:"thrown"}),o()(n)&&n.length>0){let e=i()(n).call(n,(e=>(console.error(e),e.line=e.fullPath?y(v,e.fullPath):null,e.path=e.fullPath?e.fullPath.join("."):null,e.level="error",e.type="thrown",e.source="resolver",l()(e,"message",{enumerable:!0,value:e.message}),e)));a.newThrownErrBatch(e)}return r.updateResolved(t)}))};let de=[];const me=F()((async()=>{const e=de.system;if(!e)return void console.error("debResolveSubtrees: don't have a system to operate on, aborting.");const{errActions:t,errSelectors:n,fn:{resolveSubtree:r,fetch:s,AST:a={}},specSelectors:c,specActions:p}=e;if(!r)return void console.error("Error: Swagger-Client did not provide a `resolveSubtree` method, doing nothing.");let f=a.getLineNumberForPath?a.getLineNumberForPath:()=>{};const m=c.specStr(),{modelPropertyMacro:y,parameterMacro:b,requestInterceptor:w,responseInterceptor:E}=e.getConfigs();try{var x=await u()(de).call(de,(async(e,a)=>{let{resultMap:u,specWithCurrentSubtrees:p}=await e;const{errors:x,spec:S}=await r(p,a,{baseDoc:c.url(),modelPropertyMacro:y,parameterMacro:b,requestInterceptor:w,responseInterceptor:E});if(n.allErrors().size&&t.clearBy((e=>{var t;return"thrown"!==e.get("type")||"resolver"!==e.get("source")||!h()(t=e.get("fullPath")).call(t,((e,t)=>e===a[t]||void 0===a[t]))})),o()(x)&&x.length>0){let e=i()(x).call(x,(e=>(e.line=e.fullPath?f(m,e.fullPath):null,e.path=e.fullPath?e.fullPath.join("."):null,e.level="error",e.type="thrown",e.source="resolver",l()(e,"message",{enumerable:!0,value:e.message}),e)));t.newThrownErrBatch(e)}var _,j;S&&c.isOAS3()&&"components"===a[0]&&"securitySchemes"===a[1]&&await d().all(i()(_=g()(j=v()(S)).call(j,(e=>"openIdConnect"===e.type))).call(_,(async e=>{const t={url:e.openIdConnectUrl,requestInterceptor:w,responseInterceptor:E};try{const n=await s(t);n instanceof Error||n.status>=400?console.error(n.statusText+" "+t.url):e.openIdConnectData=JSON.parse(n.text)}catch(e){console.error(e)}})));return B()(u,a,S),p=q()(a,S,p),{resultMap:u,specWithCurrentSubtrees:p}}),d().resolve({resultMap:(c.specResolvedSubtree([])||(0,P.Map)()).toJS(),specWithCurrentSubtrees:c.specJS()}));delete de.system,de=[]}catch(e){console.error(e)}p.updateResolvedSubtree([],x.resultMap)}),35),ge=e=>t=>{var n;w()(n=i()(de).call(de,(e=>e.join("@@")))).call(n,e.join("@@"))>-1||(de.push(e),de.system=t,me())};function ye(e,t,n,r,o){return{type:J,payload:{path:e,value:r,paramName:t,paramIn:n,isXml:o}}}function ve(e,t,n,r){return{type:J,payload:{path:e,param:t,value:n,isXml:r}}}const be=(e,t)=>({type:oe,payload:{path:e,value:t}}),we=()=>({type:oe,payload:{path:[],value:(0,P.Map)()}}),Ee=(e,t)=>({type:H,payload:{pathMethod:e,isOAS3:t}}),xe=(e,t,n,r)=>({type:K,payload:{pathMethod:e,paramName:t,paramIn:n,includeEmptyValue:r}});function Se(e){return{type:te,payload:{pathMethod:e}}}function _e(e,t){return{type:ne,payload:{path:e,value:t,key:"consumes_value"}}}function je(e,t){return{type:ne,payload:{path:e,value:t,key:"produces_value"}}}const Oe=(e,t,n)=>({payload:{path:e,method:t,res:n},type:G}),ke=(e,t,n)=>({payload:{path:e,method:t,req:n},type:Z}),Ae=(e,t,n)=>({payload:{path:e,method:t,req:n},type:Y}),Ce=e=>({payload:e,type:X}),Pe=e=>t=>{let{fn:n,specActions:r,specSelectors:s,getConfigs:a,oas3Selectors:l}=t,{pathName:c,method:u,operation:p}=e,{requestInterceptor:h,responseInterceptor:f}=a(),d=p.toJS();var m,y;p&&p.get("parameters")&&x()(m=g()(y=p.get("parameters")).call(y,(e=>e&&!0===e.get("allowEmptyValue")))).call(m,(t=>{if(s.parameterInclusionSettingFor([c,u],t.get("name"),t.get("in"))){e.parameters=e.parameters||{};const n=(0,U.cz)(t,e.parameters);(!n||n&&0===n.size)&&(e.parameters[t.get("name")]="")}}));if(e.contextUrl=I()(s.url()).toString(),d&&d.operationId?e.operationId=d.operationId:d&&c&&u&&(e.operationId=n.opId(d,c,u)),s.isOAS3()){const t=`${c}:${u}`;e.server=l.selectedServer(t)||l.selectedServer();const n=l.serverVariables({server:e.server,namespace:t}).toJS(),r=l.serverVariables({server:e.server}).toJS();e.serverVariables=_()(n).length?n:r,e.requestContentType=l.requestContentType(c,u),e.responseContentType=l.responseContentType(c,u)||"*/*";const s=l.requestBodyValue(c,u),a=l.requestBodyInclusionSetting(c,u);var v;if(s&&s.toJS)e.requestBody=g()(v=i()(s).call(s,(e=>P.Map.isMap(e)?e.get("value"):e))).call(v,((e,t)=>(o()(e)?0!==e.length:!(0,U.O2)(e))||a.get(t))).toJS();else e.requestBody=s}let b=O()({},e);b=n.buildRequest(b),r.setRequest(e.pathName,e.method,b);e.requestInterceptor=async t=>{let n=await h.apply(void 0,[t]),o=O()({},n);return r.setMutatedRequest(e.pathName,e.method,o),n},e.responseInterceptor=f;const w=A()();return n.execute(e).then((t=>{t.duration=A()()-w,r.setResponse(e.pathName,e.method,t)})).catch((t=>{"Failed to fetch"===t.message&&(t.name="",t.message='**Failed to fetch.** \n**Possible Reasons:** \n - CORS \n - Network Failure \n - URL scheme must be "http" or "https" for CORS request.'),r.setResponse(e.pathName,e.method,{error:!0,err:(0,T.serializeError)(t)})}))},Ne=function(){let{path:e,method:t,...n}=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return r=>{let{fn:{fetch:o},specSelectors:s,specActions:i}=r,a=s.specJsonWithResolvedSubtrees().toJS(),l=s.operationScheme(e,t),{requestContentType:c,responseContentType:u}=s.contentTypeValues([e,t]).toJS(),p=/xml/i.test(c),h=s.parameterValues([e,t],p).toJS();return i.executeRequest({...n,fetch:o,spec:a,pathName:e,method:t,parameters:h,requestContentType:c,scheme:l,responseContentType:u})}};function Ie(e,t){return{type:Q,payload:{path:e,method:t}}}function Te(e,t){return{type:ee,payload:{path:e,method:t}}}function Re(e,t,n){return{type:se,payload:{scheme:e,path:t,method:n}}}},37038:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>a});var r=n(20032),o=n(51228),s=n(33881),i=n(77508);function a(){return{statePlugins:{spec:{wrapActions:i,reducers:r.default,actions:o,selectors:s}}}}},20032:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>d});var r=n(24282),o=n.n(r),s=n(97606),i=n.n(s),a=n(76986),l=n.n(a),c=n(43393),u=n(90242),p=n(27504),h=n(33881),f=n(51228);const d={[f.UPDATE_SPEC]:(e,t)=>"string"==typeof t.payload?e.set("spec",t.payload):e,[f.UPDATE_URL]:(e,t)=>e.set("url",t.payload+""),[f.UPDATE_JSON]:(e,t)=>e.set("json",(0,u.oG)(t.payload)),[f.UPDATE_RESOLVED]:(e,t)=>e.setIn(["resolved"],(0,u.oG)(t.payload)),[f.UPDATE_RESOLVED_SUBTREE]:(e,t)=>{const{value:n,path:r}=t.payload;return e.setIn(["resolvedSubtrees",...r],(0,u.oG)(n))},[f.UPDATE_PARAM]:(e,t)=>{let{payload:n}=t,{path:r,paramName:o,paramIn:s,param:i,value:a,isXml:l}=n,c=i?(0,u.V9)(i):`${s}.${o}`;const p=l?"value_xml":"value";return e.setIn(["meta","paths",...r,"parameters",c,p],a)},[f.UPDATE_EMPTY_PARAM_INCLUSION]:(e,t)=>{let{payload:n}=t,{pathMethod:r,paramName:o,paramIn:s,includeEmptyValue:i}=n;if(!o||!s)return console.warn("Warning: UPDATE_EMPTY_PARAM_INCLUSION could not generate a paramKey."),e;const a=`${s}.${o}`;return e.setIn(["meta","paths",...r,"parameter_inclusions",a],i)},[f.VALIDATE_PARAMS]:(e,t)=>{let{payload:{pathMethod:n,isOAS3:r}}=t;const s=(0,h.specJsonWithResolvedSubtrees)(e).getIn(["paths",...n]),i=(0,h.parameterValues)(e,n).toJS();return e.updateIn(["meta","paths",...n,"parameters"],(0,c.fromJS)({}),(t=>{var a;return o()(a=s.get("parameters",(0,c.List)())).call(a,((t,o)=>{const s=(0,u.cz)(o,i),a=(0,h.parameterInclusionSettingFor)(e,n,o.get("name"),o.get("in")),l=(0,u.Ik)(o,s,{bypassRequiredCheck:a,isOAS3:r});return t.setIn([(0,u.V9)(o),"errors"],(0,c.fromJS)(l))}),t)}))},[f.CLEAR_VALIDATE_PARAMS]:(e,t)=>{let{payload:{pathMethod:n}}=t;return e.updateIn(["meta","paths",...n,"parameters"],(0,c.fromJS)([]),(e=>i()(e).call(e,(e=>e.set("errors",(0,c.fromJS)([]))))))},[f.SET_RESPONSE]:(e,t)=>{let n,{payload:{res:r,path:o,method:s}}=t;n=r.error?l()({error:!0,name:r.err.name,message:r.err.message,statusCode:r.err.statusCode},r.err.response):r,n.headers=n.headers||{};let i=e.setIn(["responses",o,s],(0,u.oG)(n));return p.Z.Blob&&r.data instanceof p.Z.Blob&&(i=i.setIn(["responses",o,s,"text"],r.data)),i},[f.SET_REQUEST]:(e,t)=>{let{payload:{req:n,path:r,method:o}}=t;return e.setIn(["requests",r,o],(0,u.oG)(n))},[f.SET_MUTATED_REQUEST]:(e,t)=>{let{payload:{req:n,path:r,method:o}}=t;return e.setIn(["mutatedRequests",r,o],(0,u.oG)(n))},[f.UPDATE_OPERATION_META_VALUE]:(e,t)=>{let{payload:{path:n,value:r,key:o}}=t,s=["paths",...n],i=["meta","paths",...n];return e.getIn(["json",...s])||e.getIn(["resolved",...s])||e.getIn(["resolvedSubtrees",...s])?e.setIn([...i,o],(0,c.fromJS)(r)):e},[f.CLEAR_RESPONSE]:(e,t)=>{let{payload:{path:n,method:r}}=t;return e.deleteIn(["responses",n,r])},[f.CLEAR_REQUEST]:(e,t)=>{let{payload:{path:n,method:r}}=t;return e.deleteIn(["requests",n,r])},[f.SET_SCHEME]:(e,t)=>{let{payload:{scheme:n,path:r,method:o}}=t;return r&&o?e.setIn(["scheme",r,o],n):r||o?void 0:e.setIn(["scheme","_defaultScheme"],n)}}},33881:(e,t,n)=>{"use strict";n.r(t),n.d(t,{allowTryItOutFor:()=>fe,basePath:()=>Q,canExecuteScheme:()=>Ae,consumes:()=>K,consumesOptionsFor:()=>Oe,contentTypeValues:()=>Se,currentProducesFor:()=>_e,definitions:()=>X,externalDocs:()=>q,findDefinition:()=>Y,getOAS3RequiredRequestBodyContentType:()=>Ne,getParameter:()=>ve,hasHost:()=>be,host:()=>ee,info:()=>$,isMediaTypeSchemaPropertiesEqual:()=>Ie,isOAS3:()=>B,lastError:()=>A,mutatedRequestFor:()=>he,mutatedRequests:()=>ce,operationScheme:()=>ke,operationWithMeta:()=>ye,operations:()=>J,operationsWithRootInherited:()=>ne,operationsWithTags:()=>se,parameterInclusionSettingFor:()=>me,parameterValues:()=>we,parameterWithMeta:()=>ge,parameterWithMetaByIdentity:()=>de,parametersIncludeIn:()=>Ee,parametersIncludeType:()=>xe,paths:()=>V,produces:()=>H,producesOptionsFor:()=>je,requestFor:()=>pe,requests:()=>le,responseFor:()=>ue,responses:()=>ae,schemes:()=>te,security:()=>G,securityDefinitions:()=>Z,semver:()=>z,spec:()=>L,specJS:()=>T,specJson:()=>I,specJsonWithResolvedSubtrees:()=>F,specResolved:()=>R,specResolvedSubtree:()=>M,specSource:()=>N,specStr:()=>P,tagDetails:()=>oe,taggedOperations:()=>ie,tags:()=>re,url:()=>C,validOperationMethods:()=>W,validateBeforeExecute:()=>Pe,validationErrors:()=>Ce,version:()=>U});var r=n(24278),o=n.n(r),s=n(86),i=n.n(s),a=n(11882),l=n.n(a),c=n(97606),u=n.n(c),p=n(14418),h=n.n(p),f=n(51679),d=n.n(f),m=n(24282),g=n.n(m),y=n(2578),v=n.n(y),b=n(92039),w=n.n(b),E=n(58309),x=n.n(E),S=n(20573),_=n(90242),j=n(43393);const O=["get","put","post","delete","options","head","patch","trace"],k=e=>e||(0,j.Map)(),A=(0,S.P1)(k,(e=>e.get("lastError"))),C=(0,S.P1)(k,(e=>e.get("url"))),P=(0,S.P1)(k,(e=>e.get("spec")||"")),N=(0,S.P1)(k,(e=>e.get("specSource")||"not-editor")),I=(0,S.P1)(k,(e=>e.get("json",(0,j.Map)()))),T=(0,S.P1)(I,(e=>e.toJS())),R=(0,S.P1)(k,(e=>e.get("resolved",(0,j.Map)()))),M=(e,t)=>e.getIn(["resolvedSubtrees",...t],void 0),D=(e,t)=>j.Map.isMap(e)&&j.Map.isMap(t)?t.get("$$ref")?t:(0,j.OrderedMap)().mergeWith(D,e,t):t,F=(0,S.P1)(k,(e=>(0,j.OrderedMap)().mergeWith(D,e.get("json"),e.get("resolvedSubtrees")))),L=e=>I(e),B=(0,S.P1)(L,(()=>!1)),$=(0,S.P1)(L,(e=>Te(e&&e.get("info")))),q=(0,S.P1)(L,(e=>Te(e&&e.get("externalDocs")))),U=(0,S.P1)($,(e=>e&&e.get("version"))),z=(0,S.P1)(U,(e=>{var t;return o()(t=/v?([0-9]*)\.([0-9]*)\.([0-9]*)/i.exec(e)).call(t,1)})),V=(0,S.P1)(F,(e=>e.get("paths"))),W=(0,S.P1)((()=>["get","put","post","delete","options","head","patch"])),J=(0,S.P1)(V,(e=>{if(!e||e.size<1)return(0,j.List)();let t=(0,j.List)();return e&&i()(e)?(i()(e).call(e,((e,n)=>{if(!e||!i()(e))return{};i()(e).call(e,((e,r)=>{l()(O).call(O,r)<0||(t=t.push((0,j.fromJS)({path:n,method:r,operation:e,id:`${r}-${n}`})))}))})),t):(0,j.List)()})),K=(0,S.P1)(L,(e=>(0,j.Set)(e.get("consumes")))),H=(0,S.P1)(L,(e=>(0,j.Set)(e.get("produces")))),G=(0,S.P1)(L,(e=>e.get("security",(0,j.List)()))),Z=(0,S.P1)(L,(e=>e.get("securityDefinitions"))),Y=(e,t)=>{const n=e.getIn(["resolvedSubtrees","definitions",t],null),r=e.getIn(["json","definitions",t],null);return n||r||null},X=(0,S.P1)(L,(e=>{const t=e.get("definitions");return j.Map.isMap(t)?t:(0,j.Map)()})),Q=(0,S.P1)(L,(e=>e.get("basePath"))),ee=(0,S.P1)(L,(e=>e.get("host"))),te=(0,S.P1)(L,(e=>e.get("schemes",(0,j.Map)()))),ne=(0,S.P1)(J,K,H,((e,t,n)=>u()(e).call(e,(e=>e.update("operation",(e=>{if(e){if(!j.Map.isMap(e))return;return e.withMutations((e=>(e.get("consumes")||e.update("consumes",(e=>(0,j.Set)(e).merge(t))),e.get("produces")||e.update("produces",(e=>(0,j.Set)(e).merge(n))),e)))}return(0,j.Map)()})))))),re=(0,S.P1)(L,(e=>{const t=e.get("tags",(0,j.List)());return j.List.isList(t)?h()(t).call(t,(e=>j.Map.isMap(e))):(0,j.List)()})),oe=(e,t)=>{var n;let r=re(e)||(0,j.List)();return d()(n=h()(r).call(r,j.Map.isMap)).call(n,(e=>e.get("name")===t),(0,j.Map)())},se=(0,S.P1)(ne,re,((e,t)=>g()(e).call(e,((e,t)=>{let n=(0,j.Set)(t.getIn(["operation","tags"]));return n.count()<1?e.update("default",(0,j.List)(),(e=>e.push(t))):g()(n).call(n,((e,n)=>e.update(n,(0,j.List)(),(e=>e.push(t)))),e)}),g()(t).call(t,((e,t)=>e.set(t.get("name"),(0,j.List)())),(0,j.OrderedMap)())))),ie=e=>t=>{var n;let{getConfigs:r}=t,{tagsSorter:o,operationsSorter:s}=r();return u()(n=se(e).sortBy(((e,t)=>t),((e,t)=>{let n="function"==typeof o?o:_.wh.tagsSorter[o];return n?n(e,t):null}))).call(n,((t,n)=>{let r="function"==typeof s?s:_.wh.operationsSorter[s],o=r?v()(t).call(t,r):t;return(0,j.Map)({tagDetails:oe(e,n),operations:o})}))},ae=(0,S.P1)(k,(e=>e.get("responses",(0,j.Map)()))),le=(0,S.P1)(k,(e=>e.get("requests",(0,j.Map)()))),ce=(0,S.P1)(k,(e=>e.get("mutatedRequests",(0,j.Map)()))),ue=(e,t,n)=>ae(e).getIn([t,n],null),pe=(e,t,n)=>le(e).getIn([t,n],null),he=(e,t,n)=>ce(e).getIn([t,n],null),fe=()=>!0,de=(e,t,n)=>{const r=F(e).getIn(["paths",...t,"parameters"],(0,j.OrderedMap)()),o=e.getIn(["meta","paths",...t,"parameters"],(0,j.OrderedMap)()),s=u()(r).call(r,(e=>{const t=o.get(`${n.get("in")}.${n.get("name")}`),r=o.get(`${n.get("in")}.${n.get("name")}.hash-${n.hashCode()}`);return(0,j.OrderedMap)().merge(e,t,r)}));return d()(s).call(s,(e=>e.get("in")===n.get("in")&&e.get("name")===n.get("name")),(0,j.OrderedMap)())},me=(e,t,n,r)=>{const o=`${r}.${n}`;return e.getIn(["meta","paths",...t,"parameter_inclusions",o],!1)},ge=(e,t,n,r)=>{const o=F(e).getIn(["paths",...t,"parameters"],(0,j.OrderedMap)()),s=d()(o).call(o,(e=>e.get("in")===r&&e.get("name")===n),(0,j.OrderedMap)());return de(e,t,s)},ye=(e,t,n)=>{var r;const o=F(e).getIn(["paths",t,n],(0,j.OrderedMap)()),s=e.getIn(["meta","paths",t,n],(0,j.OrderedMap)()),i=u()(r=o.get("parameters",(0,j.List)())).call(r,(r=>de(e,[t,n],r)));return(0,j.OrderedMap)().merge(o,s).set("parameters",i)};function ve(e,t,n,r){t=t||[];let o=e.getIn(["meta","paths",...t,"parameters"],(0,j.fromJS)([]));return d()(o).call(o,(e=>j.Map.isMap(e)&&e.get("name")===n&&e.get("in")===r))||(0,j.Map)()}const be=(0,S.P1)(L,(e=>{const t=e.get("host");return"string"==typeof t&&t.length>0&&"/"!==t[0]}));function we(e,t,n){t=t||[];let r=ye(e,...t).get("parameters",(0,j.List)());return g()(r).call(r,((e,t)=>{let r=n&&"body"===t.get("in")?t.get("value_xml"):t.get("value");return e.set((0,_.V9)(t,{allowHashes:!1}),r)}),(0,j.fromJS)({}))}function Ee(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";if(j.List.isList(e))return w()(e).call(e,(e=>j.Map.isMap(e)&&e.get("in")===t))}function xe(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";if(j.List.isList(e))return w()(e).call(e,(e=>j.Map.isMap(e)&&e.get("type")===t))}function Se(e,t){t=t||[];let n=F(e).getIn(["paths",...t],(0,j.fromJS)({})),r=e.getIn(["meta","paths",...t],(0,j.fromJS)({})),o=_e(e,t);const s=n.get("parameters")||new j.List,i=r.get("consumes_value")?r.get("consumes_value"):xe(s,"file")?"multipart/form-data":xe(s,"formData")?"application/x-www-form-urlencoded":void 0;return(0,j.fromJS)({requestContentType:i,responseContentType:o})}function _e(e,t){t=t||[];const n=F(e).getIn(["paths",...t],null);if(null===n)return;const r=e.getIn(["meta","paths",...t,"produces_value"],null),o=n.getIn(["produces",0],null);return r||o||"application/json"}function je(e,t){t=t||[];const n=F(e),r=n.getIn(["paths",...t],null);if(null===r)return;const[o]=t,s=r.get("produces",null),i=n.getIn(["paths",o,"produces"],null),a=n.getIn(["produces"],null);return s||i||a}function Oe(e,t){t=t||[];const n=F(e),r=n.getIn(["paths",...t],null);if(null===r)return;const[o]=t,s=r.get("consumes",null),i=n.getIn(["paths",o,"consumes"],null),a=n.getIn(["consumes"],null);return s||i||a}const ke=(e,t,n)=>{let r=e.get("url").match(/^([a-z][a-z0-9+\-.]*):/),o=x()(r)?r[1]:null;return e.getIn(["scheme",t,n])||e.getIn(["scheme","_defaultScheme"])||o||""},Ae=(e,t,n)=>{var r;return l()(r=["http","https"]).call(r,ke(e,t,n))>-1},Ce=(e,t)=>{t=t||[];let n=e.getIn(["meta","paths",...t,"parameters"],(0,j.fromJS)([]));const r=[];return i()(n).call(n,(e=>{let t=e.get("errors");t&&t.count()&&i()(t).call(t,(e=>r.push(e)))})),r},Pe=(e,t)=>0===Ce(e,t).length,Ne=(e,t)=>{var n;let r={requestBody:!1,requestContentType:{}},o=e.getIn(["resolvedSubtrees","paths",...t,"requestBody"],(0,j.fromJS)([]));return o.size<1||(o.getIn(["required"])&&(r.requestBody=o.getIn(["required"])),i()(n=o.getIn(["content"]).entrySeq()).call(n,(e=>{const t=e[0];if(e[1].getIn(["schema","required"])){const n=e[1].getIn(["schema","required"]).toJS();r.requestContentType[t]=n}}))),r},Ie=(e,t,n,r)=>{if((n||r)&&n===r)return!0;let o=e.getIn(["resolvedSubtrees","paths",...t,"requestBody","content"],(0,j.fromJS)([]));if(o.size<2||!n||!r)return!1;let s=o.getIn([n,"schema","properties"],(0,j.fromJS)([])),i=o.getIn([r,"schema","properties"],(0,j.fromJS)([]));return!!s.equals(i)};function Te(e){return j.Map.isMap(e)?e:new j.Map}},77508:(e,t,n)=>{"use strict";n.r(t),n.d(t,{executeRequest:()=>p,updateJsonSpec:()=>u,updateSpec:()=>c,validateParams:()=>h});var r=n(28222),o=n.n(r),s=n(86),i=n.n(s),a=n(27361),l=n.n(a);const c=(e,t)=>{let{specActions:n}=t;return function(){e(...arguments),n.parseToJson(...arguments)}},u=(e,t)=>{let{specActions:n}=t;return function(){for(var t=arguments.length,r=new Array(t),s=0;s{l()(c,[e]).$ref&&n.requestResolvedSubtree(["paths",e])})),n.requestResolvedSubtree(["components","securitySchemes"])}},p=(e,t)=>{let{specActions:n}=t;return t=>(n.logRequest(t),e(t))},h=(e,t)=>{let{specSelectors:n}=t;return t=>e(t,n.isOAS3())}},34852:(e,t,n)=>{"use strict";n.r(t),n.d(t,{loaded:()=>r});const r=(e,t)=>function(){e(...arguments);const n=t.getConfigs().withCredentials;void 0!==n&&(t.fn.fetch.withCredentials="string"==typeof n?"true"===n:!!n)}},79934:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>BE});var r={};n.r(r),n.d(r,{JsonPatchError:()=>j,_areEquals:()=>M,applyOperation:()=>P,applyPatch:()=>N,applyReducer:()=>I,deepClone:()=>O,getValueByPointer:()=>C,validate:()=>R,validator:()=>T});var o={};n.r(o),n.d(o,{compare:()=>z,generate:()=>q,observe:()=>$,unobserve:()=>B});var s={};n.r(s),n.d(s,{hasElementSourceMap:()=>Cs,includesClasses:()=>Ns,includesSymbols:()=>Ps,isAnnotationElement:()=>_s,isArrayElement:()=>ws,isBooleanElement:()=>vs,isCommentElement:()=>js,isElement:()=>ds,isLinkElement:()=>xs,isMemberElement:()=>Es,isNullElement:()=>ys,isNumberElement:()=>gs,isObjectElement:()=>bs,isParseResultElement:()=>Os,isPrimitiveElement:()=>As,isRefElement:()=>Ss,isSourceMapElement:()=>ks,isStringElement:()=>ms});var i={};n.r(i),n.d(i,{isJSONReferenceElement:()=>cc,isJSONSchemaElement:()=>lc,isLinkDescriptionElement:()=>pc,isMediaElement:()=>uc});var a={};n.r(a),n.d(a,{isOpenApi3_0LikeElement:()=>$c,isOpenApiExtension:()=>Kc,isParameterLikeElement:()=>qc,isReferenceLikeElement:()=>Uc,isRequestBodyLikeElement:()=>zc,isResponseLikeElement:()=>Vc,isServerLikeElement:()=>Wc,isTagLikeElement:()=>Jc});var l={};n.r(l),n.d(l,{isBooleanJsonSchemaElement:()=>ap,isCallbackElement:()=>Lu,isComponentsElement:()=>Bu,isContactElement:()=>$u,isExampleElement:()=>qu,isExternalDocumentationElement:()=>Uu,isHeaderElement:()=>zu,isInfoElement:()=>Vu,isLicenseElement:()=>Wu,isLinkElement:()=>Ju,isLinkElementExternal:()=>Ku,isMediaTypeElement:()=>pp,isOpenApi3_0Element:()=>Gu,isOpenapiElement:()=>Hu,isOperationElement:()=>Zu,isParameterElement:()=>Yu,isPathItemElement:()=>Xu,isPathItemElementExternal:()=>Qu,isPathsElement:()=>ep,isReferenceElement:()=>tp,isReferenceElementExternal:()=>np,isRequestBodyElement:()=>rp,isResponseElement:()=>op,isResponsesElement:()=>sp,isSchemaElement:()=>ip,isSecurityRequirementElement:()=>lp,isServerElement:()=>cp,isServerVariableElement:()=>up});var c={};n.r(c),n.d(c,{isBooleanJsonSchemaElement:()=>Kg,isCallbackElement:()=>Sg,isComponentsElement:()=>_g,isContactElement:()=>jg,isExampleElement:()=>Og,isExternalDocumentationElement:()=>kg,isHeaderElement:()=>Ag,isInfoElement:()=>Cg,isJsonSchemaDialectElement:()=>Pg,isLicenseElement:()=>Ng,isLinkElement:()=>Ig,isLinkElementExternal:()=>Tg,isMediaTypeElement:()=>Yg,isOpenApi3_1Element:()=>Mg,isOpenapiElement:()=>Rg,isOperationElement:()=>Dg,isParameterElement:()=>Fg,isPathItemElement:()=>Lg,isPathItemElementExternal:()=>Bg,isPathsElement:()=>$g,isReferenceElement:()=>qg,isReferenceElementExternal:()=>Ug,isRequestBodyElement:()=>zg,isResponseElement:()=>Vg,isResponsesElement:()=>Wg,isSchemaElement:()=>Jg,isSecurityRequirementElement:()=>Hg,isServerElement:()=>Gg,isServerVariableElement:()=>Zg});var u={};n.r(u),n.d(u,{cookie:()=>EE,header:()=>wE,path:()=>yE,query:()=>vE});var p,h=n(58826),f=n.n(h),d=(p=function(e,t){return p=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},p(e,t)},function(e,t){function n(){this.constructor=e}p(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),m=Object.prototype.hasOwnProperty;function g(e,t){return m.call(e,t)}function y(e){if(Array.isArray(e)){for(var t=new Array(e.length),n=0;n=48&&t<=57))return!1;n++}return!0}function w(e){return-1===e.indexOf("/")&&-1===e.indexOf("~")?e:e.replace(/~/g,"~0").replace(/\//g,"~1")}function E(e){return e.replace(/~1/g,"/").replace(/~0/g,"~")}function x(e){if(void 0===e)return!0;if(e)if(Array.isArray(e)){for(var t=0,n=e.length;t0&&"constructor"==a[c-1]))throw new TypeError("JSON-Patch: modifying `__proto__` or `constructor/prototype` prop is banned for security reasons, if this was on purpose, please set `banPrototypeModifications` flag false and pass it to this function. More info in fast-json-patch README");if(n&&void 0===p&&(void 0===l[h]?p=a.slice(0,c).join("/"):c==u-1&&(p=t.path),void 0!==p&&f(t,0,e,p)),c++,Array.isArray(l)){if("-"===h)h=l.length;else{if(n&&!b(h))throw new j("Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index","OPERATION_PATH_ILLEGAL_ARRAY_INDEX",s,t,e);b(h)&&(h=~~h)}if(c>=u){if(n&&"add"===t.op&&h>l.length)throw new j("The specified index MUST NOT be greater than the number of elements in the array","OPERATION_VALUE_OUT_OF_BOUNDS",s,t,e);if(!1===(i=A[t.op].call(t,l,h,e)).test)throw new j("Test operation failed","TEST_OPERATION_FAILED",s,t,e);return i}}else if(c>=u){if(!1===(i=k[t.op].call(t,l,h,e)).test)throw new j("Test operation failed","TEST_OPERATION_FAILED",s,t,e);return i}if(l=l[h],n&&c0)throw new j('Operation `path` property must start with "/"',"OPERATION_PATH_INVALID",t,e,n);if(("move"===e.op||"copy"===e.op)&&"string"!=typeof e.from)throw new j("Operation `from` property is not present (applicable in `move` and `copy` operations)","OPERATION_FROM_REQUIRED",t,e,n);if(("add"===e.op||"replace"===e.op||"test"===e.op)&&void 0===e.value)throw new j("Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)","OPERATION_VALUE_REQUIRED",t,e,n);if(("add"===e.op||"replace"===e.op||"test"===e.op)&&x(e.value))throw new j("Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)","OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED",t,e,n);if(n)if("add"==e.op){var o=e.path.split("/").length,s=r.split("/").length;if(o!==s+1&&o!==s)throw new j("Cannot perform an `add` operation at the desired path","OPERATION_PATH_CANNOT_ADD",t,e,n)}else if("replace"===e.op||"remove"===e.op||"_get"===e.op){if(e.path!==r)throw new j("Cannot perform the operation at a path that does not exist","OPERATION_PATH_UNRESOLVABLE",t,e,n)}else if("move"===e.op||"copy"===e.op){var i=R([{op:"_get",path:e.from,value:void 0}],n);if(i&&"OPERATION_PATH_UNRESOLVABLE"===i.name)throw new j("Cannot perform the operation from a path that does not exist","OPERATION_FROM_UNRESOLVABLE",t,e,n)}}function R(e,t,n){try{if(!Array.isArray(e))throw new j("Patch sequence must be an array","SEQUENCE_NOT_AN_ARRAY");if(t)N(v(t),v(e),n||!0);else{n=n||T;for(var r=0;r0&&(e.patches=[],e.callback&&e.callback(r)),r}function U(e,t,n,r,o){if(t!==e){"function"==typeof t.toJSON&&(t=t.toJSON());for(var s=y(t),i=y(e),a=!1,l=i.length-1;l>=0;l--){var c=e[p=i[l]];if(!g(t,p)||void 0===t[p]&&void 0!==c&&!1===Array.isArray(t))Array.isArray(e)===Array.isArray(t)?(o&&n.push({op:"test",path:r+"/"+w(p),value:v(c)}),n.push({op:"remove",path:r+"/"+w(p)}),a=!0):(o&&n.push({op:"test",path:r,value:e}),n.push({op:"replace",path:r,value:t}),!0);else{var u=t[p];"object"==typeof c&&null!=c&&"object"==typeof u&&null!=u&&Array.isArray(c)===Array.isArray(u)?U(c,u,n,r+"/"+w(p),o):c!==u&&(!0,o&&n.push({op:"test",path:r+"/"+w(p),value:v(c)}),n.push({op:"replace",path:r+"/"+w(p),value:v(u)}))}}if(a||s.length!=i.length)for(l=0;lvoid 0!==t&&e?e[t]:e),e)},applyPatch:function(e,t,n){if(n=n||{},"merge"===(t=f()(f()({},t),{},{path:t.path&&K(t.path)})).op){const n=ae(e,t.path);Object.assign(n,t.value),N(e,[H(t.path,n)])}else if("mergeDeep"===t.op){const n=ae(e,t.path),r=W()(n,t.value);e=N(e,[H(t.path,r)]).newDocument}else if("add"===t.op&&""===t.path&&te(t.value)){N(e,Object.keys(t.value).reduce(((e,n)=>(e.push({op:"add",path:`/${K(n)}`,value:t.value[n]}),e)),[]))}else if("replace"===t.op&&""===t.path){let{value:r}=t;n.allowMetaPatches&&t.meta&&se(t)&&(Array.isArray(t.value)||te(t.value))&&(r=f()(f()({},r),t.meta)),e=r}else if(N(e,[t]),n.allowMetaPatches&&t.meta&&se(t)&&(Array.isArray(t.value)||te(t.value))){const n=ae(e,t.path),r=f()(f()({},n),t.meta);N(e,[H(t.path,r)])}return e},parentPathMatch:function(e,t){if(!Array.isArray(t))return!1;for(let n=0,r=t.length;n(e+"").replace(/~/g,"~0").replace(/\//g,"~1"))).join("/")}`:e}function H(e,t,n){return{op:"replace",path:e,value:t,meta:n}}function G(e,t,n){return ee(Q(e.filter(se).map((e=>t(e.value,n,e.path)))||[]))}function Z(e,t,n){return n=n||[],Array.isArray(e)?e.map(((e,r)=>Z(e,t,n.concat(r)))):te(e)?Object.keys(e).map((r=>Z(e[r],t,n.concat(r)))):t(e,n[n.length-1],n)}function Y(e,t,n){let r=[];if((n=n||[]).length>0){const o=t(e,n[n.length-1],n);o&&(r=r.concat(o))}if(Array.isArray(e)){const o=e.map(((e,r)=>Y(e,t,n.concat(r))));o&&(r=r.concat(o))}else if(te(e)){const o=Object.keys(e).map((r=>Y(e[r],t,n.concat(r))));o&&(r=r.concat(o))}return r=Q(r),r}function X(e){return Array.isArray(e)?e:[e]}function Q(e){return[].concat(...e.map((e=>Array.isArray(e)?Q(e):e)))}function ee(e){return e.filter((e=>void 0!==e))}function te(e){return e&&"object"==typeof e}function ne(e){return e&&"function"==typeof e}function re(e){if(ie(e)){const{op:t}=e;return"add"===t||"remove"===t||"replace"===t}return!1}function oe(e){return re(e)||ie(e)&&"mutation"===e.type}function se(e){return oe(e)&&("add"===e.op||"replace"===e.op||"merge"===e.op||"mergeDeep"===e.op)}function ie(e){return e&&"object"==typeof e}function ae(e,t){try{return C(e,t)}catch(e){return console.error(e),{}}}n(31905);var le=n(1272),ce=n(8575);function ue(e,t){function n(){Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack;for(var e=arguments.length,n=new Array(e),r=0;r-1&&-1===de.indexOf(n)||me.indexOf(r)>-1||ge.some((e=>r.indexOf(e)>-1))}function ve(e,t){const[n,r]=e.split("#"),o=ce.resolve(n||"",t||"");return r?`${o}#${r}`:o}const be="application/json, application/yaml",we=/^([a-z]+:\/\/|\/\/)/i,Ee=ue("JSONRefError",(function(e,t,n){this.originalError=n,Object.assign(this,t||{})})),xe={},Se=new WeakMap,_e=[e=>"paths"===e[0]&&"responses"===e[3]&&"examples"===e[5],e=>"paths"===e[0]&&"responses"===e[3]&&"content"===e[5]&&"example"===e[7],e=>"paths"===e[0]&&"responses"===e[3]&&"content"===e[5]&&"examples"===e[7]&&"value"===e[9],e=>"paths"===e[0]&&"requestBody"===e[3]&&"content"===e[4]&&"example"===e[6],e=>"paths"===e[0]&&"requestBody"===e[3]&&"content"===e[4]&&"examples"===e[6]&&"value"===e[8],e=>"paths"===e[0]&&"parameters"===e[2]&&"example"===e[4],e=>"paths"===e[0]&&"parameters"===e[3]&&"example"===e[5],e=>"paths"===e[0]&&"parameters"===e[2]&&"examples"===e[4]&&"value"===e[6],e=>"paths"===e[0]&&"parameters"===e[3]&&"examples"===e[5]&&"value"===e[7],e=>"paths"===e[0]&&"parameters"===e[2]&&"content"===e[4]&&"example"===e[6],e=>"paths"===e[0]&&"parameters"===e[2]&&"content"===e[4]&&"examples"===e[6]&&"value"===e[8],e=>"paths"===e[0]&&"parameters"===e[3]&&"content"===e[4]&&"example"===e[7],e=>"paths"===e[0]&&"parameters"===e[3]&&"content"===e[5]&&"examples"===e[7]&&"value"===e[9]],je={key:"$ref",plugin:(e,t,n,r)=>{const o=r.getInstance(),s=n.slice(0,-1);if(ye(s)||(e=>_e.some((t=>t(e))))(s))return;const{baseDoc:i}=r.getContext(n);if("string"!=typeof e)return new Ee("$ref: must be a string (JSON-Ref)",{$ref:e,baseDoc:i,fullPath:n});const a=Pe(e),l=a[0],c=a[1]||"";let u,p,h;try{u=i||l?Ae(l,i):null}catch(t){return Ce(t,{pointer:c,$ref:e,basePath:u,fullPath:n})}if(function(e,t,n,r){let o=Se.get(r);o||(o={},Se.set(r,o));const s=function(e){if(0===e.length)return"";return`/${e.map(De).join("/")}`}(n),i=`${t||""}#${e}`,a=s.replace(/allOf\/\d+\/?/g,""),l=r.contextTree.get([]).baseDoc;if(t===l&&Le(a,e))return!0;let c="";const u=n.some((e=>(c=`${c}/${De(e)}`,o[c]&&o[c].some((e=>Le(e,i)||Le(i,e))))));if(u)return!0;return void(o[a]=(o[a]||[]).concat(i))}(c,u,s,r)&&!o.useCircularStructures){const t=ve(e,u);return e===t?null:J.replace(n,t)}if(null==u?(h=Re(c),p=r.get(h),void 0===p&&(p=new Ee(`Could not resolve reference: ${e}`,{pointer:c,$ref:e,baseDoc:i,fullPath:n}))):(p=Ne(u,c),p=null!=p.__value?p.__value:p.catch((t=>{throw Ce(t,{pointer:c,$ref:e,baseDoc:i,fullPath:n})}))),p instanceof Error)return[J.remove(n),p];const f=ve(e,u),d=J.replace(s,p,{$$ref:f});if(u&&u!==i)return[d,J.context(s,{baseDoc:u})];try{if(!function(e,t){const n=[e];return t.path.reduce(((e,t)=>(n.push(e[t]),e[t])),e),r(t.value);function r(e){return J.isObject(e)&&(n.indexOf(e)>=0||Object.keys(e).some((t=>r(e[t]))))}}(r.state,d)||o.useCircularStructures)return d}catch(e){return null}}},Oe=Object.assign(je,{docCache:xe,absoluteify:Ae,clearCache:function(e){void 0!==e?delete xe[e]:Object.keys(xe).forEach((e=>{delete xe[e]}))},JSONRefError:Ee,wrapError:Ce,getDoc:Ie,split:Pe,extractFromDoc:Ne,fetchJSON:function(e){return fetch(e,{headers:{Accept:be},loadSpec:!0}).then((e=>e.text())).then((e=>le.ZP.load(e)))},extract:Te,jsonPointerToArray:Re,unescapeJsonPointerToken:Me}),ke=Oe;function Ae(e,t){if(!we.test(e)){if(!t)throw new Ee(`Tried to resolve a relative URL, without having a basePath. path: '${e}' basePath: '${t}'`);return ce.resolve(t,e)}return e}function Ce(e,t){let n;return n=e&&e.response&&e.response.body?`${e.response.body.code} ${e.response.body.message}`:e.message,new Ee(`Could not resolve reference: ${n}`,t,e)}function Pe(e){return(e+"").split("#")}function Ne(e,t){const n=xe[e];if(n&&!J.isPromise(n))try{const e=Te(t,n);return Object.assign(Promise.resolve(e),{__value:e})}catch(e){return Promise.reject(e)}return Ie(e).then((e=>Te(t,e)))}function Ie(e){const t=xe[e];return t?J.isPromise(t)?t:Promise.resolve(t):(xe[e]=Oe.fetchJSON(e).then((t=>(xe[e]=t,t))),xe[e])}function Te(e,t){const n=Re(e);if(n.length<1)return t;const r=J.getIn(t,n);if(void 0===r)throw new Ee(`Could not resolve pointer: ${e} does not exist in document`,{pointer:e});return r}function Re(e){if("string"!=typeof e)throw new TypeError("Expected a string, got a "+typeof e);return"/"===e[0]&&(e=e.substr(1)),""===e?[]:e.split("/").map(Me)}function Me(e){if("string"!=typeof e)return e;return new URLSearchParams(`=${e.replace(/~1/g,"/").replace(/~0/g,"~")}`).get("")}function De(e){return new URLSearchParams([["",e.replace(/~/g,"~0").replace(/\//g,"~1")]]).toString().slice(1)}const Fe=e=>!e||"/"===e||"#"===e;function Le(e,t){if(Fe(t))return!0;const n=e.charAt(t.length),r=t.slice(-1);return 0===e.indexOf(t)&&(!n||"/"===n||"#"===n)&&"#"!==r}const Be={key:"allOf",plugin:(e,t,n,r,o)=>{if(o.meta&&o.meta.$$ref)return;const s=n.slice(0,-1);if(ye(s))return;if(!Array.isArray(e)){const e=new TypeError("allOf must be an array");return e.fullPath=n,e}let i=!1,a=o.value;if(s.forEach((e=>{a&&(a=a[e])})),a=f()({},a),0===Object.keys(a).length)return;delete a.allOf;const l=[];return l.push(r.replace(s,{})),e.forEach(((e,t)=>{if(!r.isObject(e)){if(i)return null;i=!0;const e=new TypeError("Elements in allOf must be objects");return e.fullPath=n,l.push(e)}l.push(r.mergeDeep(s,e));const o=function(e,t){let{specmap:n,getBaseUrlForNodePath:r=(e=>n.getContext([...t,...e]).baseDoc),targetKeys:o=["$ref","$$ref"]}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const s=[];return he()(e).forEach((function(){if(o.includes(this.key)&&"string"==typeof this.node){const e=this.path,o=t.concat(this.path),i=ve(this.node,r(e));s.push(n.replace(o,i))}})),s}(e,n.slice(0,-1),{getBaseUrlForNodePath:e=>r.getContext([...n,t,...e]).baseDoc,specmap:r});l.push(...o)})),a.example&&l.push(r.remove([].concat(s,"example"))),l.push(r.mergeDeep(s,a)),a.$$ref||l.push(r.remove([].concat(s,"$$ref"))),l}},$e={key:"parameters",plugin:(e,t,n,r)=>{if(Array.isArray(e)&&e.length){const t=Object.assign([],e),o=n.slice(0,-1),s=f()({},J.getIn(r.spec,o));for(let o=0;o{const o=f()({},e);for(const t in e)try{o[t].default=r.modelPropertyMacro(o[t])}catch(e){const t=new Error(e);return t.fullPath=n,t}return J.replace(n,o)}};class Ue{constructor(e){this.root=ze(e||{})}set(e,t){const n=this.getParent(e,!0);if(!n)return void Ve(this.root,t,null);const r=e[e.length-1],{children:o}=n;o[r]?Ve(o[r],t,n):o[r]=ze(t,n)}get(e){if((e=e||[]).length<1)return this.root.value;let t,n,r=this.root;for(let o=0;o{if(!e)return e;const{children:r}=e;return!r[n]&&t&&(r[n]=ze(null,e)),r[n]}),this.root)}}function ze(e,t){return Ve({children:{}},e,t)}function Ve(e,t,n){return e.value=t||{},e.protoValue=n?f()(f()({},n.protoValue),e.value):e.value,Object.keys(e.children).forEach((t=>{const n=e.children[t];e.children[t]=Ve(n,n.value,e)})),e}const We=()=>{};class Je{static getPluginName(e){return e.pluginName}static getPatchesOfType(e,t){return e.filter(t)}constructor(e){Object.assign(this,{spec:"",debugLevel:"info",plugins:[],pluginHistory:{},errors:[],mutations:[],promisedPatches:[],state:{},patches:[],context:{},contextTree:new Ue,showDebug:!1,allPatches:[],pluginProp:"specMap",libMethods:Object.assign(Object.create(this),J,{getInstance:()=>this}),allowMetaPatches:!1},e),this.get=this._get.bind(this),this.getContext=this._getContext.bind(this),this.hasRun=this._hasRun.bind(this),this.wrappedPlugins=this.plugins.map(this.wrapPlugin.bind(this)).filter(J.isFunction),this.patches.push(J.add([],this.spec)),this.patches.push(J.context([],this.context)),this.updatePatches(this.patches)}debug(e){if(this.debugLevel===e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r1?t-1:0),r=1;r!Array.isArray(e)||e.every(((e,n)=>e===t[n]));return function*(r,o){const s={};for(const e of r.filter(J.isAdditiveMutation))yield*i(e.value,e.path,e);function*i(r,a,l){if(J.isObject(r)){const c=a.length-1,u=a[c],p=a.indexOf("properties"),h="properties"===u&&c===p,f=o.allowMetaPatches&&s[r.$$ref];for(const c of Object.keys(r)){const u=r[c],p=a.concat(c),d=J.isObject(u),m=r.$$ref;if(f||d&&(o.allowMetaPatches&&m&&(s[m]=!0),yield*i(u,p,l)),!h&&c===e.key){const r=t(n,a);n&&!r||(yield e.plugin(u,c,p,o,l))}}}else e.key===a[a.length-1]&&(yield e.plugin(r,e.key,a,o))}}}(e)),Object.assign(r.bind(o),{pluginName:e.name||t,isGenerator:J.isGenerator(r)})}nextPlugin(){return this.wrappedPlugins.find((e=>this.getMutationsForPlugin(e).length>0))}nextPromisedPatch(){if(this.promisedPatches.length>0)return Promise.race(this.promisedPatches.map((e=>e.value)))}getPluginHistory(e){const t=this.constructor.getPluginName(e);return this.pluginHistory[t]||[]}getPluginRunCount(e){return this.getPluginHistory(e).length}getPluginHistoryTip(e){const t=this.getPluginHistory(e);return t&&t[t.length-1]||{}}getPluginMutationIndex(e){const t=this.getPluginHistoryTip(e).mutationIndex;return"number"!=typeof t?-1:t}updatePluginHistory(e,t){const n=this.constructor.getPluginName(e);this.pluginHistory[n]=this.pluginHistory[n]||[],this.pluginHistory[n].push(t)}updatePatches(e){J.normalizeArray(e).forEach((e=>{if(e instanceof Error)this.errors.push(e);else try{if(!J.isObject(e))return void this.debug("updatePatches","Got a non-object patch",e);if(this.showDebug&&this.allPatches.push(e),J.isPromise(e.value))return this.promisedPatches.push(e),void this.promisedPatchThen(e);if(J.isContextPatch(e))return void this.setContext(e.path,e.value);J.isMutation(e)&&this.updateMutations(e)}catch(e){console.error(e),this.errors.push(e)}}))}updateMutations(e){"object"==typeof e.value&&!Array.isArray(e.value)&&this.allowMetaPatches&&(e.value=f()({},e.value));const t=J.applyPatch(this.state,e,{allowMetaPatches:this.allowMetaPatches});t&&(this.mutations.push(e),this.state=t)}removePromisedPatch(e){const t=this.promisedPatches.indexOf(e);t<0?this.debug("Tried to remove a promisedPatch that isn't there!"):this.promisedPatches.splice(t,1)}promisedPatchThen(e){return e.value=e.value.then((t=>{const n=f()(f()({},e),{},{value:t});this.removePromisedPatch(e),this.updatePatches(n)})).catch((t=>{this.removePromisedPatch(e),this.updatePatches(t)})),e.value}getMutations(e,t){return e=e||0,"number"!=typeof t&&(t=this.mutations.length),this.mutations.slice(e,t)}getCurrentMutations(){return this.getMutationsForPlugin(this.getCurrentPlugin())}getMutationsForPlugin(e){const t=this.getPluginMutationIndex(e);return this.getMutations(t+1)}getCurrentPlugin(){return this.currentPlugin}getLib(){return this.libMethods}_get(e){return J.getIn(this.state,e)}_getContext(e){return this.contextTree.get(e)}setContext(e,t){return this.contextTree.set(e,t)}_hasRun(e){return this.getPluginRunCount(this.getCurrentPlugin())>(e||0)}dispatch(){const e=this,t=this.nextPlugin();if(!t){const e=this.nextPromisedPatch();if(e)return e.then((()=>this.dispatch())).catch((()=>this.dispatch()));const t={spec:this.state,errors:this.errors};return this.showDebug&&(t.patches=this.allPatches),Promise.resolve(t)}if(e.pluginCount=e.pluginCount||{},e.pluginCount[t]=(e.pluginCount[t]||0)+1,e.pluginCount[t]>100)return Promise.resolve({spec:e.state,errors:e.errors.concat(new Error("We've reached a hard limit of 100 plugin runs"))});if(t!==this.currentPlugin&&this.promisedPatches.length){const e=this.promisedPatches.map((e=>e.value));return Promise.all(e.map((e=>e.then(We,We)))).then((()=>this.dispatch()))}return function(){e.currentPlugin=t;const r=e.getCurrentMutations(),o=e.mutations.length-1;try{if(t.isGenerator)for(const o of t(r,e.getLib()))n(o);else{n(t(r,e.getLib()))}}catch(e){console.error(e),n([Object.assign(Object.create(e),{plugin:t})])}finally{e.updatePluginHistory(t,{mutationIndex:o})}return e.dispatch()}();function n(n){n&&(n=J.fullyNormalizeArray(n),e.updatePatches(n,t))}}}const Ke={refs:ke,allOf:Be,parameters:$e,properties:qe};var He=n(32454);function Ge(e){const{spec:t}=e,{paths:n}=t,r={};if(!n||t.$$normalized)return e;for(const e in n){const o=n[e];if(null==o||!["object","function"].includes(typeof o))continue;const s=o.parameters;for(const n in o){const i=o[n];if(null==i||!["object","function"].includes(typeof i))continue;const a=(0,He.Z)(i,e,n);if(a){r[a]?r[a].push(i):r[a]=[i];const e=r[a];if(e.length>1)e.forEach(((e,t)=>{e.__originalOperationId=e.__originalOperationId||e.operationId,e.operationId=`${a}${t+1}`}));else if(void 0!==i.operationId){const t=e[0];t.__originalOperationId=t.__originalOperationId||i.operationId,t.operationId=a}}if("parameters"!==n){const e=[],n={};for(const r in t)"produces"!==r&&"consumes"!==r&&"security"!==r||(n[r]=t[r],e.push(n));if(s&&(n.parameters=s,e.push(n)),e.length)for(const t of e)for(const e in t)if(i[e]){if("parameters"===e)for(const n of t[e]){i[e].some((e=>e.name&&e.name===n.name||e.$ref&&e.$ref===n.$ref||e.$$ref&&e.$$ref===n.$$ref||e===n))||i[e].push(n)}}else i[e]=t[e]}}}return t.$$normalized=!0,e}function Ze(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const{requestInterceptor:n,responseInterceptor:r}=t,o=e.withCredentials?"include":"same-origin";return t=>e({url:t,loadSpec:!0,requestInterceptor:n,responseInterceptor:r,headers:{Accept:be},credentials:o}).then((e=>e.body))}var Ye=n(80129),Xe=n.n(Ye);const Qe="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:window,{FormData:et,Blob:tt,File:nt}=Qe,rt=e=>":/?#[]@!$&'()*+,;=".indexOf(e)>-1,ot=e=>/^[a-z0-9\-._~]+$/i.test(e);function st(e){let{escape:t}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=arguments.length>2?arguments[2]:void 0;return"number"==typeof e&&(e=e.toString()),"string"==typeof e&&e.length&&t?n?JSON.parse(e):[...e].map((e=>{if(ot(e))return e;if(rt(e)&&"unsafe"===t)return e;const n=new TextEncoder;return Array.from(n.encode(e)).map((e=>`0${e.toString(16).toUpperCase()}`.slice(-2))).map((e=>`%${e}`)).join("")})).join(""):e}function it(e){const{value:t}=e;return Array.isArray(t)?function(e){let{key:t,value:n,style:r,explode:o,escape:s}=e;const i=e=>st(e,{escape:s});if("simple"===r)return n.map((e=>i(e))).join(",");if("label"===r)return`.${n.map((e=>i(e))).join(".")}`;if("matrix"===r)return n.map((e=>i(e))).reduce(((e,n)=>!e||o?`${e||""};${t}=${n}`:`${e},${n}`),"");if("form"===r){const e=o?`&${t}=`:",";return n.map((e=>i(e))).join(e)}if("spaceDelimited"===r){const e=o?`${t}=`:"";return n.map((e=>i(e))).join(` ${e}`)}if("pipeDelimited"===r){const e=o?`${t}=`:"";return n.map((e=>i(e))).join(`|${e}`)}return}(e):"object"==typeof t?function(e){let{key:t,value:n,style:r,explode:o,escape:s}=e;const i=e=>st(e,{escape:s}),a=Object.keys(n);if("simple"===r)return a.reduce(((e,t)=>{const r=i(n[t]);return`${e?`${e},`:""}${t}${o?"=":","}${r}`}),"");if("label"===r)return a.reduce(((e,t)=>{const r=i(n[t]);return`${e?`${e}.`:"."}${t}${o?"=":"."}${r}`}),"");if("matrix"===r&&o)return a.reduce(((e,t)=>`${e?`${e};`:";"}${t}=${i(n[t])}`),"");if("matrix"===r)return a.reduce(((e,r)=>{const o=i(n[r]);return`${e?`${e},`:`;${t}=`}${r},${o}`}),"");if("form"===r)return a.reduce(((e,t)=>{const r=i(n[t]);return`${e?`${e}${o?"&":","}`:""}${t}${o?"=":","}${r}`}),"");return}(e):function(e){let{key:t,value:n,style:r,escape:o}=e;const s=e=>st(e,{escape:o});if("simple"===r)return s(n);if("label"===r)return`.${s(n)}`;if("matrix"===r)return`;${t}=${s(n)}`;if("form"===r)return s(n);if("deepObject"===r)return s(n,{},!0);return}(e)}const at=(e,t)=>{t.body=e},lt={serializeRes:pt,mergeInQueryOrForm:wt};async function ct(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};"object"==typeof e&&(t=e,e=t.url),t.headers=t.headers||{},lt.mergeInQueryOrForm(t),t.headers&&Object.keys(t.headers).forEach((e=>{const n=t.headers[e];"string"==typeof n&&(t.headers[e]=n.replace(/\n+/g," "))})),t.requestInterceptor&&(t=await t.requestInterceptor(t)||t);const n=t.headers["content-type"]||t.headers["Content-Type"];let r;/multipart\/form-data/i.test(n)&&t.body instanceof et&&(delete t.headers["content-type"],delete t.headers["Content-Type"]);try{r=await(t.userFetch||fetch)(t.url,t),r=await lt.serializeRes(r,e,t),t.responseInterceptor&&(r=await t.responseInterceptor(r)||r)}catch(e){if(!r)throw e;const t=new Error(r.statusText||`response status is ${r.status}`);throw t.status=r.status,t.statusCode=r.status,t.responseError=e,t}if(!r.ok){const e=new Error(r.statusText||`response status is ${r.status}`);throw e.status=r.status,e.statusCode=r.status,e.response=r,e}return r}const ut=function(){return/(json|xml|yaml|text)\b/.test(arguments.length>0&&void 0!==arguments[0]?arguments[0]:"")};function pt(e,t){let{loadSpec:n=!1}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const r={ok:e.ok,url:e.url||t,status:e.status,statusText:e.statusText,headers:ht(e.headers)},o=r.headers["content-type"],s=n||ut(o);return(s?e.text:e.blob||e.buffer).call(e).then((e=>{if(r.text=e,r.data=e,s)try{const t=function(e,t){return t&&(0===t.indexOf("application/json")||t.indexOf("+json")>0)?JSON.parse(e):le.ZP.load(e)}(e,o);r.body=t,r.obj=t}catch(e){r.parseError=e}return r}))}function ht(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return"function"!=typeof e.entries?{}:Array.from(e.entries()).reduce(((e,t)=>{let[n,r]=t;return e[n]=function(e){return e.includes(", ")?e.split(", "):e}(r),e}),{})}function ft(e,t){return t||"undefined"==typeof navigator||(t=navigator),t&&"ReactNative"===t.product?!(!e||"object"!=typeof e||"string"!=typeof e.uri):void 0!==nt&&e instanceof nt||(void 0!==tt&&e instanceof tt||(!!ArrayBuffer.isView(e)||null!==e&&"object"==typeof e&&"function"==typeof e.pipe))}function dt(e,t){return Array.isArray(e)&&e.some((e=>ft(e,t)))}const mt={form:",",spaceDelimited:"%20",pipeDelimited:"|"},gt={csv:",",ssv:"%20",tsv:"%09",pipes:"|"};function yt(e,t){let n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];const{collectionFormat:r,allowEmptyValue:o,serializationOption:s,encoding:i}=t,a="object"!=typeof t||Array.isArray(t)?t:t.value,l=n?e=>e.toString():e=>encodeURIComponent(e),c=l(e);if(void 0===a&&o)return[[c,""]];if(ft(a)||dt(a))return[[c,a]];if(s)return vt(e,a,n,s);if(i){if([typeof i.style,typeof i.explode,typeof i.allowReserved].some((e=>"undefined"!==e))){const{style:t,explode:r,allowReserved:o}=i;return vt(e,a,n,{style:t,explode:r,allowReserved:o})}if(i.contentType){if("application/json"===i.contentType){return[[c,l("string"==typeof a?a:JSON.stringify(a))]]}return[[c,l(a.toString())]]}return"object"!=typeof a?[[c,l(a)]]:Array.isArray(a)&&a.every((e=>"object"!=typeof e))?[[c,a.map(l).join(",")]]:[[c,l(JSON.stringify(a))]]}return"object"!=typeof a?[[c,l(a)]]:Array.isArray(a)?"multi"===r?[[c,a.map(l)]]:[[c,a.map(l).join(gt[r||"csv"])]]:[[c,""]]}function vt(e,t,n,r){const o=r.style||"form",s=void 0===r.explode?"form"===o:r.explode,i=!n&&(r&&r.allowReserved?"unsafe":"reserved"),a=e=>st(e,{escape:i}),l=n?e=>e:e=>st(e,{escape:i});return"object"!=typeof t?[[l(e),a(t)]]:Array.isArray(t)?s?[[l(e),t.map(a)]]:[[l(e),t.map(a).join(mt[o])]]:"deepObject"===o?Object.keys(t).map((n=>[l(`${e}[${n}]`),a(t[n])])):s?Object.keys(t).map((e=>[l(e),a(t[e])])):[[l(e),Object.keys(t).map((e=>[`${l(e)},${a(t[e])}`])).join(",")]]}function bt(e){const t=Object.keys(e).reduce(((t,n)=>{for(const[r,o]of yt(n,e[n]))t[r]=o;return t}),{});return Xe().stringify(t,{encode:!1,indices:!1})||""}function wt(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};const{url:t="",query:n,form:r}=e;if(r){const t=Object.keys(r).some((e=>{const{value:t}=r[e];return ft(t)||dt(t)})),n=e.headers["content-type"]||e.headers["Content-Type"];if(t||/multipart\/form-data/i.test(n)){const t=(o=e.form,Object.entries(o).reduce(((e,t)=>{let[n,r]=t;for(const[t,o]of yt(n,r,!0))if(Array.isArray(o))for(const n of o)if(ArrayBuffer.isView(n)){const r=new tt([n]);e.append(t,r)}else e.append(t,n);else if(ArrayBuffer.isView(o)){const n=new tt([o]);e.append(t,n)}else e.append(t,o);return e}),new et));at(t,e)}else e.body=bt(r);delete e.form}var o;if(n){const[r,o]=t.split("?");let s="";if(o){const e=Xe().parse(o);Object.keys(n).forEach((t=>delete e[t])),s=Xe().stringify(e,{encode:!0})}const i=function(){for(var e=arguments.length,t=new Array(e),n=0;ne)).join("&");return r?`?${r}`:""}(s,bt(n));e.url=r+i,delete e.query}return e}const Et=e=>{const{baseDoc:t,url:n}=e;return t||n||""},xt=e=>{const{fetch:t,http:n}=e;return t||n||ct};async function St(e){const{spec:t,mode:n,allowMetaPatches:r=!0,pathDiscriminator:o,modelPropertyMacro:s,parameterMacro:i,requestInterceptor:a,responseInterceptor:l,skipNormalization:c,useCircularStructures:u}=e,p=Et(e),h=xt(e);return function(e){p&&(Ke.refs.docCache[p]=e);Ke.refs.fetchJSON=Ze(h,{requestInterceptor:a,responseInterceptor:l});const t=[Ke.refs];"function"==typeof i&&t.push(Ke.parameters);"function"==typeof s&&t.push(Ke.properties);"strict"!==n&&t.push(Ke.allOf);return(f={spec:e,context:{baseDoc:p},plugins:t,allowMetaPatches:r,pathDiscriminator:o,parameterMacro:i,modelPropertyMacro:s,useCircularStructures:u},new Je(f).dispatch()).then(c?async e=>e:Ge);var f}(t)}const _t={name:"generic",match:()=>!0,normalize(e){let{spec:t}=e;const{spec:n}=Ge({spec:t});return n},resolve:async e=>St(e)};const jt=e=>{try{const{openapi:t}=e;return"string"==typeof t&&/^3\.0\.([0123])(?:-rc[012])?$/.test(t)}catch{return!1}},Ot=e=>{try{const{openapi:t}=e;return"string"==typeof t&&/^3\.1\.(?:[1-9]\d*|0)$/.test(t)}catch{return!1}},kt=e=>jt(e)||Ot(e),At={name:"openapi-2",match(e){let{spec:t}=e;return(e=>{try{const{swagger:t}=e;return"2.0"===t}catch{return!1}})(t)},normalize(e){let{spec:t}=e;const{spec:n}=Ge({spec:t});return n},resolve:async e=>async function(e){return St(e)}(e)};const Ct={name:"openapi-3-0",match(e){let{spec:t}=e;return jt(t)},normalize(e){let{spec:t}=e;const{spec:n}=Ge({spec:t});return n},resolve:async e=>async function(e){return St(e)}(e)};var Pt=n(43500);class Nt extends Pt.RP{constructor(e,t,n){super(e,t,n),this.element="annotation"}get code(){return this.attributes.get("code")}set code(e){this.attributes.set("code",e)}}const It=Nt;class Tt extends Pt.RP{constructor(e,t,n){super(e,t,n),this.element="comment"}}const Rt=Tt;const Mt=function(){return!1};const Dt=function(){return!0};function Ft(e){return null!=e&&"object"==typeof e&&!0===e["@@functional/placeholder"]}function Lt(e){return function t(n){return 0===arguments.length||Ft(n)?t:e.apply(this,arguments)}}function Bt(e){return function t(n,r){switch(arguments.length){case 0:return t;case 1:return Ft(n)?t:Lt((function(t){return e(n,t)}));default:return Ft(n)&&Ft(r)?t:Ft(n)?Lt((function(t){return e(t,r)})):Ft(r)?Lt((function(t){return e(n,t)})):e(n,r)}}}const $t=Array.isArray||function(e){return null!=e&&e.length>=0&&"[object Array]"===Object.prototype.toString.call(e)};function qt(e,t,n){return function(){if(0===arguments.length)return n();var r=arguments[arguments.length-1];if(!$t(r)){for(var o=0;o=arguments.length)?a=t[i]:(a=arguments[o],o+=1),r[i]=a,Ft(a)||(s-=1),i+=1}return s<=0?n.apply(this,r):Ht(s,Gt(e,r,n))}}const Zt=Bt((function(e,t){return 1===e?Lt(t):Ht(e,Gt(e,[],t))}));function Yt(e){for(var t,n=[];!(t=e.next()).done;)n.push(t.value);return n}function Xt(e,t,n){for(var r=0,o=n.length;r=0;)Qt(t=on[n],e)&&!an(r,t)&&(r[r.length]=t),n-=1;return r})):Lt((function(e){return Object(e)!==e?[]:Object.keys(e)}));const cn=Lt((function(e){return null===e?"Null":void 0===e?"Undefined":Object.prototype.toString.call(e).slice(8,-1)}));function un(e,t,n,r){var o=Yt(e);function s(e,t){return pn(e,t,n.slice(),r.slice())}return!Xt((function(e,t){return!Xt(s,t,e)}),Yt(t),o)}function pn(e,t,n,r){if(en(e,t))return!0;var o,s,i=cn(e);if(i!==cn(t))return!1;if("function"==typeof e["fantasy-land/equals"]||"function"==typeof t["fantasy-land/equals"])return"function"==typeof e["fantasy-land/equals"]&&e["fantasy-land/equals"](t)&&"function"==typeof t["fantasy-land/equals"]&&t["fantasy-land/equals"](e);if("function"==typeof e.equals||"function"==typeof t.equals)return"function"==typeof e.equals&&e.equals(t)&&"function"==typeof t.equals&&t.equals(e);switch(i){case"Arguments":case"Array":case"Object":if("function"==typeof e.constructor&&"Promise"===(o=e.constructor,null==(s=String(o).match(/^function (\w*)/))?"":s[1]))return e===t;break;case"Boolean":case"Number":case"String":if(typeof e!=typeof t||!en(e.valueOf(),t.valueOf()))return!1;break;case"Date":if(!en(e.valueOf(),t.valueOf()))return!1;break;case"Error":return e.name===t.name&&e.message===t.message;case"RegExp":if(e.source!==t.source||e.global!==t.global||e.ignoreCase!==t.ignoreCase||e.multiline!==t.multiline||e.sticky!==t.sticky||e.unicode!==t.unicode)return!1}for(var a=n.length-1;a>=0;){if(n[a]===e)return r[a]===t;a-=1}switch(i){case"Map":return e.size===t.size&&un(e.entries(),t.entries(),n.concat([e]),r.concat([t]));case"Set":return e.size===t.size&&un(e.values(),t.values(),n.concat([e]),r.concat([t]));case"Arguments":case"Array":case"Object":case"Boolean":case"Number":case"String":case"Date":case"Error":case"RegExp":case"Int8Array":case"Uint8Array":case"Uint8ClampedArray":case"Int16Array":case"Uint16Array":case"Int32Array":case"Uint32Array":case"Float32Array":case"Float64Array":case"ArrayBuffer":break;default:return!1}var l=ln(e);if(l.length!==ln(t).length)return!1;var c=n.concat([e]),u=r.concat([t]);for(a=l.length-1;a>=0;){var p=l[a];if(!Qt(p,t)||!pn(t[p],e[p],c,u))return!1;a-=1}return!0}const hn=Bt((function(e,t){return pn(e,t,[],[])}));function fn(e,t){return function(e,t,n){var r,o;if("function"==typeof e.indexOf)switch(typeof t){case"number":if(0===t){for(r=1/t;n=0}function dn(e,t){for(var n=0,r=t.length,o=Array(r);n":jn(n,r)},r=function(e,t){return dn((function(t){return mn(t)+": "+n(e[t])}),t.slice().sort())};switch(Object.prototype.toString.call(e)){case"[object Arguments]":return"(function() { return arguments; }("+dn(n,e).join(", ")+"))";case"[object Array]":return"["+dn(n,e).concat(r(e,_n((function(e){return/^\d+$/.test(e)}),ln(e)))).join(", ")+"]";case"[object Boolean]":return"object"==typeof e?"new Boolean("+n(e.valueOf())+")":e.toString();case"[object Date]":return"new Date("+(isNaN(e.valueOf())?n(NaN):mn(yn(e)))+")";case"[object Map]":return"new Map("+n(Array.from(e))+")";case"[object Null]":return"null";case"[object Number]":return"object"==typeof e?"new Number("+n(e.valueOf())+")":1/e==-1/0?"-0":e.toString(10);case"[object Set]":return"new Set("+n(Array.from(e).sort())+")";case"[object String]":return"object"==typeof e?"new String("+n(e.valueOf())+")":mn(e);case"[object Undefined]":return"undefined";default:if("function"==typeof e.toString){var o=e.toString();if("[object Object]"!==o)return o}return"{"+r(e,ln(e)).join(", ")+"}"}}const On=Lt((function(e){return jn(e,[])}));const kn=Bt((function(e,t){if(e===t)return t;function n(e,t){if(e>t!=t>e)return t>e?t:e}var r=n(e,t);if(void 0!==r)return r;var o=n(typeof e,typeof t);if(void 0!==o)return o===typeof e?e:t;var s=On(e),i=n(s,On(t));return void 0!==i&&i===s?e:t}));var An=function(){function e(e,t){this.xf=t,this.f=e}return e.prototype["@@transducer/init"]=zt,e.prototype["@@transducer/result"]=Vt,e.prototype["@@transducer/step"]=function(e,t){return this.xf["@@transducer/step"](e,this.f(t))},e}();const Cn=Bt(qt(["fantasy-land/map","map"],(function(e){return function(t){return new An(e,t)}}),(function(e,t){switch(Object.prototype.toString.call(t)){case"[object Function]":return Zt(t.length,(function(){return e.call(this,t.apply(this,arguments))}));case"[object Object]":return bn((function(n,r){return n[r]=e(t[r]),n}),{},ln(t));default:return dn(e,t)}}))),Pn=Number.isInteger||function(e){return e<<0===e};function Nn(e){return"[object String]"===Object.prototype.toString.call(e)}const In=Bt((function(e,t){var n=e<0?t.length+e:e;return Nn(t)?t.charAt(n):t[n]}));const Tn=Bt((function(e,t){if(null!=t)return Pn(e)?In(e,t):t[e]}));const Rn=Bt((function(e,t){return Cn(Tn(e),t)}));function Mn(e){return function t(n,r,o){switch(arguments.length){case 0:return t;case 1:return Ft(n)?t:Bt((function(t,r){return e(n,t,r)}));case 2:return Ft(n)&&Ft(r)?t:Ft(n)?Bt((function(t,n){return e(t,r,n)})):Ft(r)?Bt((function(t,r){return e(n,t,r)})):Lt((function(t){return e(n,r,t)}));default:return Ft(n)&&Ft(r)&&Ft(o)?t:Ft(n)&&Ft(r)?Bt((function(t,n){return e(t,n,o)})):Ft(n)&&Ft(o)?Bt((function(t,n){return e(t,r,n)})):Ft(r)&&Ft(o)?Bt((function(t,r){return e(n,t,r)})):Ft(n)?Lt((function(t){return e(t,r,o)})):Ft(r)?Lt((function(t){return e(n,t,o)})):Ft(o)?Lt((function(t){return e(n,r,t)})):e(n,r,o)}}}const Dn=Lt((function(e){return!!$t(e)||!!e&&("object"==typeof e&&(!Nn(e)&&(0===e.length||e.length>0&&(e.hasOwnProperty(0)&&e.hasOwnProperty(e.length-1)))))}));var Fn="undefined"!=typeof Symbol?Symbol.iterator:"@@iterator";function Ln(e,t,n){return function(r,o,s){if(Dn(s))return e(r,o,s);if(null==s)return o;if("function"==typeof s["fantasy-land/reduce"])return t(r,o,s,"fantasy-land/reduce");if(null!=s[Fn])return n(r,o,s[Fn]());if("function"==typeof s.next)return n(r,o,s);if("function"==typeof s.reduce)return t(r,o,s,"reduce");throw new TypeError("reduce: list must be array or iterable")}}function Bn(e,t,n){for(var r=0,o=n.length;r1){var s=!rr(r)&&Qt(o,r)&&"object"==typeof r[o]?r[o]:Pn(t[1])?[]:{};n=e(Array.prototype.slice.call(t,1),n,s)}return function(e,t,n){if(Pn(e)&&$t(n)){var r=[].concat(n);return r[e]=t,r}var o={};for(var s in n)o[s]=n[s];return o[e]=t,o}(o,n,r)}));function sr(e){var t=Object.prototype.toString.call(e);return"[object Function]"===t||"[object AsyncFunction]"===t||"[object GeneratorFunction]"===t||"[object AsyncGeneratorFunction]"===t}const ir=Bt((function(e,t){return e&&t}));const ar=Bt((function(e,t){var n=Zt(e,t);return Zt(e,(function(){return bn(Qn,Cn(n,arguments[0]),Array.prototype.slice.call(arguments,1))}))}));const lr=Lt((function(e){return ar(e.length,e)}));const cr=Bt((function(e,t){return sr(e)?function(){return e.apply(this,arguments)&&t.apply(this,arguments)}:lr(ir)(e,t)}));const ur=Lt((function(e){return function(t,n){return e(t,n)?-1:e(n,t)?1:0}}));const pr=lr(Lt((function(e){return!e})));function hr(e,t){return function(){return t.call(this,e.apply(this,arguments))}}function fr(e,t){return function(){var n=arguments.length;if(0===n)return t();var r=arguments[n-1];return $t(r)||"function"!=typeof r[e]?t.apply(this,arguments):r[e].apply(r,Array.prototype.slice.call(arguments,0,n-1))}}const dr=Mn(fr("slice",(function(e,t,n){return Array.prototype.slice.call(n,e,t)})));const mr=Lt(fr("tail",dr(1,1/0)));function gr(){if(0===arguments.length)throw new Error("pipe requires at least one argument");return Ht(arguments[0].length,Jn(hr,arguments[0],mr(arguments)))}var yr=Bt((function(e,t){return Zt(Jn(kn,0,Rn("length",t)),(function(){var n=arguments,r=this;return e.apply(r,dn((function(e){return e.apply(r,n)}),t))}))}));const vr=yr;function br(e){return new RegExp(e.source,e.flags?e.flags:(e.global?"g":"")+(e.ignoreCase?"i":"")+(e.multiline?"m":"")+(e.sticky?"y":"")+(e.unicode?"u":"")+(e.dotAll?"s":""))}function wr(e,t,n){if(n||(n=new Er),function(e){var t=typeof e;return null==e||"object"!=t&&"function"!=t}(e))return e;var r=function(r){var o=n.get(e);if(o)return o;for(var s in n.set(e,r),e)Object.prototype.hasOwnProperty.call(e,s)&&(r[s]=t?wr(e[s],!0,n):e[s]);return r};switch(cn(e)){case"Object":return r(Object.create(Object.getPrototypeOf(e)));case"Array":return r([]);case"Date":return new Date(e.valueOf());case"RegExp":return br(e);case"Int8Array":case"Uint8Array":case"Uint8ClampedArray":case"Int16Array":case"Uint16Array":case"Int32Array":case"Uint32Array":case"Float32Array":case"Float64Array":case"BigInt64Array":case"BigUint64Array":return e.slice();default:return e}}var Er=function(){function e(){this.map={},this.length=0}return e.prototype.set=function(e,t){const n=this.hash(e);let r=this.map[n];r||(this.map[n]=r=[]),r.push([e,t]),this.length+=1},e.prototype.hash=function(e){let t=[];for(var n in e)t.push(Object.prototype.toString.call(e[n]));return t.join()},e.prototype.get=function(e){if(this.length<=180){for(const t in this.map){const n=this.map[t];for(let t=0;t=0&&this.i>=this.n?Ut(n):n},e}();function Ir(e){return function(t){return new Nr(e,t)}}const Tr=Bt(qt(["take"],Ir,(function(e,t){return dr(0,e<0?1/0:e,t)})));function Rr(e,t){for(var n=t.length-1;n>=0&&e(t[n]);)n-=1;return dr(0,n+1,t)}var Mr=function(){function e(e,t){this.f=e,this.retained=[],this.xf=t}return e.prototype["@@transducer/init"]=zt,e.prototype["@@transducer/result"]=function(e){return this.retained=null,this.xf["@@transducer/result"](e)},e.prototype["@@transducer/step"]=function(e,t){return this.f(t)?this.retain(e,t):this.flush(e,t)},e.prototype.flush=function(e,t){return e=zn(this.xf,e,this.retained),this.retained=[],this.xf["@@transducer/step"](e,t)},e.prototype.retain=function(e,t){return this.retained.push(t),e},e}();function Dr(e){return function(t){return new Mr(e,t)}}const Fr=Bt(qt([],Dr,Rr));var Lr=function(){function e(e,t){this.xf=t,this.f=e}return e.prototype["@@transducer/init"]=zt,e.prototype["@@transducer/result"]=Vt,e.prototype["@@transducer/step"]=function(e,t){if(this.f){if(this.f(t))return e;this.f=null}return this.xf["@@transducer/step"](e,t)},e}();function Br(e){return function(t){return new Lr(e,t)}}const $r=Bt(qt(["dropWhile"],Br,(function(e,t){for(var n=0,r=t.length;ne.classes.contains("api"))).first}get results(){return this.children.filter((e=>e.classes.contains("result")))}get result(){return this.results.first}get annotations(){return this.children.filter((e=>"annotation"===e.element))}get warnings(){return this.children.filter((e=>"annotation"===e.element&&e.classes.contains("warning")))}get errors(){return this.children.filter((e=>"annotation"===e.element&&e.classes.contains("error")))}get isEmpty(){return this.children.reject((e=>"annotation"===e.element)).isEmpty}replaceResult(e){const{result:t}=this;if(qo(t))return!1;const n=this.content.findIndex((e=>e===t));return-1!==n&&(this.content[n]=e,!0)}}const zo=Uo;class Vo extends Pt.ON{constructor(e,t,n){super(e,t,n),this.element="sourceMap"}get positionStart(){return this.children.filter((e=>e.classes.contains("position"))).get(0)}get positionEnd(){return this.children.filter((e=>e.classes.contains("position"))).get(1)}set position(e){if(null===e)return;const t=new Pt.ON([e.start.row,e.start.column,e.start.char]),n=new Pt.ON([e.end.row,e.end.column,e.end.char]);t.classes.push("position"),n.classes.push("position"),this.push(t).push(n)}}const Wo=Vo;var Jo=n(80621),Ko=n(52201),Ho=n(27398);function Go(e){return Go="function"==typeof Ko&&"symbol"==typeof Ho?function(e){return typeof e}:function(e){return e&&"function"==typeof Ko&&e.constructor===Ko&&e!==Ko.prototype?"symbol":typeof e},Go(e)}var Zo=n(26189);function Yo(e){var t=function(e,t){if("object"!==Go(e)||null===e)return e;var n=e[Zo];if(void 0!==n){var r=n.call(e,t||"default");if("object"!==Go(r))return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"===Go(t)?t:String(t)}function Xo(e,t,n){return(t=Yo(t))in e?Jo(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}const Qo=Zt(1,gr(cn,Xr("GeneratorFunction")));const es=Zt(1,gr(cn,Xr("AsyncFunction")));const ts=Gn([gr(cn,Xr("Function")),Qo,es]);const ns=pr(ts);const rs=Zt(1,ts(Array.isArray)?Array.isArray:gr(cn,Xr("Array")));const os=cr(rs,so);var ss=Zt(3,(function(e,t,n){var r=uo(e,n),o=uo(ro(e),n);if(!ns(r)&&!os(e)){var s=$n(r,o);return er(s,t)}}));const is=ss;const as=Wr(no),ls=(e,t)=>"function"==typeof(null==t?void 0:t[e]),cs=e=>null!=e&&Object.prototype.hasOwnProperty.call(e,"_storedElement")&&Object.prototype.hasOwnProperty.call(e,"_content"),us=(e,t)=>{var n;return(null==t||null===(n=t.primitive)||void 0===n?void 0:n.call(t))===e},ps=(e,t)=>{var n,r;return(null==t||null===(n=t.classes)||void 0===n||null===(r=n.includes)||void 0===r?void 0:r.call(n,e))||!1},hs=(e,t)=>(null==t?void 0:t.element)===e,fs=e=>e({hasMethod:ls,hasBasicElementProps:cs,primitiveEq:us,isElementType:hs,hasClass:ps}),ds=fs((({hasBasicElementProps:e,primitiveEq:t})=>n=>n instanceof Pt.W_||e(n)&&t(void 0,n))),ms=fs((({hasBasicElementProps:e,primitiveEq:t})=>n=>n instanceof Pt.RP||e(n)&&t("string",n))),gs=fs((({hasBasicElementProps:e,primitiveEq:t})=>n=>n instanceof Pt.VL||e(n)&&t("number",n))),ys=fs((({hasBasicElementProps:e,primitiveEq:t})=>n=>n instanceof Pt.zr||e(n)&&t("null",n))),vs=fs((({hasBasicElementProps:e,primitiveEq:t})=>n=>n instanceof Pt.hh||e(n)&&t("boolean",n))),bs=fs((({hasBasicElementProps:e,primitiveEq:t,hasMethod:n})=>r=>r instanceof Pt.Sb||e(r)&&t("object",r)&&n("keys",r)&&n("values",r)&&n("items",r))),ws=fs((({hasBasicElementProps:e,primitiveEq:t,hasMethod:n})=>r=>r instanceof Pt.ON&&!(r instanceof Pt.Sb)||e(r)&&t("array",r)&&n("push",r)&&n("unshift",r)&&n("map",r)&&n("reduce",r))),Es=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Pt.c6||e(r)&&t("member",r)&&n(void 0,r))),xs=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Pt.EA||e(r)&&t("link",r)&&n(void 0,r))),Ss=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Pt.tK||e(r)&&t("ref",r)&&n(void 0,r))),_s=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof It||e(r)&&t("annotation",r)&&n("array",r))),js=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Rt||e(r)&&t("comment",r)&&n("string",r))),Os=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof zo||e(r)&&t("parseResult",r)&&n("array",r))),ks=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Wo||e(r)&&t("sourceMap",r)&&n("array",r))),As=e=>hs("object",e)||hs("array",e)||hs("boolean",e)||hs("number",e)||hs("string",e)||hs("null",e)||hs("member",e),Cs=e=>{var t,n;return ks(null==e||null===(t=e.meta)||void 0===t||null===(n=t.get)||void 0===n?void 0:n.call(t,"sourceMap"))},Ps=(e,t)=>{if(0===e.length)return!0;const n=t.attributes.get("symbols");return!!ws(n)&&Kt(as(n.toValue()),e)},Ns=(e,t)=>0===e.length||Kt(as(t.classes.toValue()),e);const Is=hn(null);const Ts=pr(Is);function Rs(e){return Rs="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Rs(e)}const Ms=function(e){return"object"===Rs(e)};const Ds=Zt(1,cr(Ts,Ms));var Fs=gr(cn,Xr("Object")),Ls=gr(On,hn(On(Object))),Bs=wo(cr(ts,Ls),["constructor"]);const $s=Zt(1,(function(e){if(!Ds(e)||!Fs(e))return!1;var t=Object.getPrototypeOf(e);return!!Is(t)||Bs(t)}));class qs extends Pt.lS{constructor(){super(),this.register("annotation",It),this.register("comment",Rt),this.register("parseResult",zo),this.register("sourceMap",Wo)}}const Us=new qs,zs=e=>{const t=new qs;return $s(e)&&t.use(e),t},Vs=Us;function Ws(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}const Js=()=>({predicates:function(e){for(var t=1;t=0||(o[n]=e[n]);return o}(e,t);if(Ks){var s=Ks(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}var Ys=n(43992);const Xs=Zt(1,gr(cn,Xr("String"))),Qs=(e,t,n)=>{const r=e[t];if(null!=r){if(!n&&"function"==typeof r)return r;const e=n?r.leave:r.enter;if("function"==typeof e)return e}else{const r=n?e.leave:e.enter;if(null!=r){if("function"==typeof r)return r;const e=r[t];if("function"==typeof e)return e}}return null},ei={},ti=e=>null==e?void 0:e.type,ni=e=>"string"==typeof ti(e),ri=(e,{visitFnGetter:t=Qs,nodeTypeGetter:n=ti}={})=>{const r=new Array(e.length);return{enter(o,...s){for(let i=0;i{const p=n||{};let h,f,d=Array.isArray(e),m=[e],g=-1,y=[];const v=[],b=[];let w=e;do{g+=1;const e=g===m.length;let n,E;const x=e&&0!==y.length;if(e){if(n=0===b.length?void 0:v.pop(),E=f,f=b.pop(),x){E=d?E.slice():Object.create(Object.getPrototypeOf(E),Object.getOwnPropertyDescriptors(E));let e=0;for(let t=0;t{const p=n||{};let h,f,d=Array.isArray(e),m=[e],g=-1,y=[];const v=[],b=[];let w=e;do{g+=1;const e=g===m.length;let n,E;const x=e&&0!==y.length;if(e){if(n=0===b.length?void 0:v.pop(),E=f,f=b.pop(),x){E=d?E.slice():Object.create(Object.getPrototypeOf(E),Object.getOwnPropertyDescriptors(E));let e=0;for(let t=0;tbs(e)?"ObjectElement":ws(e)?"ArrayElement":Es(e)?"MemberElement":ms(e)?"StringElement":vs(e)?"BooleanElement":gs(e)?"NumberElement":ys(e)?"NullElement":xs(e)?"LinkElement":Ss(e)?"RefElement":void 0,ui=gr(ci,Xs),pi={ObjectElement:["content"],ArrayElement:["content"],MemberElement:["key","value"],StringElement:[],BooleanElement:[],NumberElement:[],NullElement:[],RefElement:[],LinkElement:[],Annotation:[],Comment:[],ParseResultElement:["content"],SourceMap:["content"]},hi=Ys({props:{result:[],predicate:Mt,returnOnTrue:void 0,returnOnFalse:void 0},init({predicate:e=this.predicate,returnOnTrue:t=this.returnOnTrue,returnOnFalse:n=this.returnOnFalse}={}){this.result=[],this.predicate=e,this.returnOnTrue=t,this.returnOnFalse=n},methods:{enter(e){return this.predicate(e)?(this.result.push(e),this.returnOnTrue):this.returnOnFalse}}}),fi=(e,t,n={})=>{let{keyMap:r=pi}=n,o=Zs(n,si);return oi(e,t,li({keyMap:r,nodeTypeGetter:ci,nodePredicate:ui},o))};fi[Symbol.for("nodejs.util.promisify.custom")]=async(e,t,n={})=>{let{keyMap:r=pi}=n,o=Zs(n,ii);return oi[Symbol.for("nodejs.util.promisify.custom")](e,t,li({keyMap:r,nodeTypeGetter:ci,nodePredicate:ui},o))};const di=(e,t,n={})=>{if(0===t.length)return e;const r=So(Js,"toolboxCreator",n),o=So({},"visitorOptions",n),s=So(ci,"nodeTypeGetter",o),i=r(),a=t.map((e=>e(i))),l=ri(a.map(So({},"visitor")),{nodeTypeGetter:s});a.forEach(is(["pre"],[]));const c=fi(e,l,o);return a.forEach(is(["post"],[])),c};function mi(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function gi(e){for(var t=1;t{const r=new t(e);return di(r,n,{toolboxCreator:Js,visitorOptions:{nodeTypeGetter:ci}})},vi=e=>(t,n={})=>yi(t,gi(gi({},n),{},{Type:e}));Pt.Sb.refract=vi(Pt.Sb),Pt.ON.refract=vi(Pt.ON),Pt.RP.refract=vi(Pt.RP),Pt.hh.refract=vi(Pt.hh),Pt.zr.refract=vi(Pt.zr),Pt.VL.refract=vi(Pt.VL),Pt.EA.refract=vi(Pt.EA),Pt.tK.refract=vi(Pt.tK),It.refract=vi(It),Rt.refract=vi(Rt),zo.refract=vi(zo),Wo.refract=vi(Wo);const bi=(e,t=new WeakMap)=>(Es(e)?(t.set(e.key,e),bi(e.key,t),t.set(e.value,e),bi(e.value,t)):e.children.forEach((n=>{t.set(n,e),bi(n,t)})),t),wi=Ys.init((function({element:e}){let t;this.transclude=function(n,r){var o;if(n===e)return r;if(n===r)return e;t=null!==(o=t)&&void 0!==o?o:bi(e);const s=t.get(n);return qo(s)?void 0:(bs(s)?((e,t,n)=>{const r=n.get(e);bs(r)&&(r.content=r.map(((o,s,i)=>i===e?(n.delete(e),n.set(t,r),t):i)))})(n,r,t):ws(s)?((e,t,n)=>{const r=n.get(e);ws(r)&&(r.content=r.map((o=>o===e?(n.delete(e),n.set(t,r),t):o)))})(n,r,t):Es(s)&&((e,t,n)=>{const r=n.get(e);Es(r)&&(r.key===e&&(r.key=t,n.delete(e),n.set(t,r)),r.value===e&&(r.value=t,n.delete(e),n.set(t,r)))})(n,r,t),e)}})),Ei=wi,xi=["keyMap"],Si=["keyMap"];function _i(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function ji(e){for(var t=1;t"string"==typeof(null==e?void 0:e.type)?e.type:ci(e),ki=ji({EphemeralObject:["content"],EphemeralArray:["content"]},pi),Ai=(e,t,n={})=>{let{keyMap:r=ki}=n,o=Zs(n,xi);return fi(e,t,ji({keyMap:r,nodeTypeGetter:Oi,nodePredicate:Dt,detectCycles:!1,deleteNodeSymbol:Symbol.for("delete-node"),skipVisitingNodeSymbol:Symbol.for("skip-visiting-node")},o))};Ai[Symbol.for("nodejs.util.promisify.custom")]=async(e,t={})=>{let{keyMap:n=ki}=t,r=Zs(t,Si);return fi[Symbol.for("nodejs.util.promisify.custom")](e,visitor,ji({keyMap:n,nodeTypeGetter:Oi,nodePredicate:Dt,detectCycles:!1,deleteNodeSymbol:Symbol.for("delete-node"),skipVisitingNodeSymbol:Symbol.for("skip-visiting-node")},r))};const Ci=class{constructor(e){Xo(this,"type","EphemeralArray"),Xo(this,"content",[]),Xo(this,"reference",void 0),this.content=e,this.reference=[]}toReference(){return this.reference}toArray(){return this.reference.push(...this.content),this.reference}};const Pi=class{constructor(e){Xo(this,"type","EphemeralObject"),Xo(this,"content",[]),Xo(this,"reference",void 0),this.content=e,this.reference={}}toReference(){return this.reference}toObject(){return Object.assign(this.reference,Object.fromEntries(this.content))}},Ni=Ys.init((function(){const e=new WeakMap;this.BooleanElement=function(e){return e.toValue()},this.NumberElement=function(e){return e.toValue()},this.StringElement=function(e){return e.toValue()},this.NullElement=function(){return null},this.ObjectElement={enter(t){if(e.has(t))return e.get(t).toReference();const n=new Pi(t.content);return e.set(t,n),n}},this.EphemeralObject={leave:e=>e.toObject()},this.MemberElement={enter:e=>[e.key,e.value]},this.ArrayElement={enter(t){if(e.has(t))return e.get(t).toReference();const n=new Ci(t.content);return e.set(t,n),n}},this.EphemeralArray={leave:e=>e.toArray()}})),Ii=(e,t=Vs)=>{if(Xs(e))try{return t.fromRefract(JSON.parse(e))}catch{}return $s(e)&&Hr("element",e)?t.fromRefract(e):t.toElement(e)},Ti=e=>Ai(e,Ni());const Ri=hn("");var Mi=cr(Zt(1,gr(cn,Xr("Number"))),isFinite);var Di=Zt(1,Mi);var Fi=cr(ts(Number.isFinite)?Zt(1,$n(Number.isFinite,Number)):Di,vr(hn,[Math.floor,eo]));var Li=Zt(1,Fi);const Bi=ts(Number.isInteger)?Zt(1,$n(Number.isInteger,Number)):Li;var $i=Or((function(e,t){return gr(Io(""),$r(as(e)),io(""))(t)}));const qi=$i;class Ui extends Error{constructor(e){super(`Invalid $ref pointer "${e}". Pointers must begin with "/"`),this.name=this.constructor.name,this.message=`Invalid $ref pointer "${e}". Pointers must begin with "/"`,"function"==typeof Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=new Error(`Invalid $ref pointer "${e}". Pointers must begin with "/"`).stack}}class zi extends Error{constructor(e){super(e),this.name=this.constructor.name,this.message=e,"function"==typeof Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=new Error(e).stack}}const Vi=gr(Co(/~/g,"~0"),Co(/\//g,"~1"),encodeURIComponent),Wi=gr(Co(/~1/g,"/"),Co(/~0/g,"~"),(e=>{try{return decodeURIComponent(e)}catch{return e}})),Ji=(e,t)=>{const n=(e=>{if(Ri(e))return[];if(!To("/",e))throw new Ui(e);const t=gr(Io("/"),Cn(Wi))(e);return mr(t)})(e);return n.reduce(((e,t)=>{if(bs(e)){if(!e.hasKey(t))throw new zi(`Evaluation failed on token: "${t}"`);return e.get(t)}if(ws(e)){if(!(t in e.content)||!Bi(Number(t)))throw new zi(`Evaluation failed on token: "${t}"`);return e.get(Number(t))}throw new zi(`Evaluation failed on token: "${t}"`)}),t)},Ki=e=>{const t=(e=>{const t=e.indexOf("#");return-1!==t?e.substring(t):"#"})(e);return qi("#",t)};class Hi extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="callback"}}const Gi=Hi;class Zi extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="components"}get schemas(){return this.get("schemas")}set schemas(e){this.set("schemas",e)}get responses(){return this.get("responses")}set responses(e){this.set("responses",e)}get parameters(){return this.get("parameters")}set parameters(e){this.set("parameters",e)}get examples(){return this.get("examples")}set examples(e){this.set("examples",e)}get requestBodies(){return this.get("requestBodies")}set requestBodies(e){this.set("requestBodies",e)}get headers(){return this.get("headers")}set headers(e){this.set("headers",e)}get securitySchemes(){return this.get("securitySchemes")}set securitySchemes(e){this.set("securitySchemes",e)}get links(){return this.get("links")}set links(e){this.set("links",e)}get callbacks(){return this.get("callbacks")}set callbacks(e){this.set("callbacks",e)}}const Yi=Zi;class Xi extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="contact"}get name(){return this.get("name")}set name(e){this.set("name",e)}get url(){return this.get("url")}set url(e){this.set("url",e)}get email(){return this.get("email")}set email(e){this.set("email",e)}}const Qi=Xi;class ea extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="discriminator"}get propertyName(){return this.get("propertyName")}set propertyName(e){this.set("propertyName",e)}get mapping(){return this.get("mapping")}set mapping(e){this.set("mapping",e)}}const ta=ea;class na extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="encoding"}get contentType(){return this.get("contentType")}set contentType(e){this.set("contentType",e)}get headers(){return this.get("headers")}set headers(e){this.set("headers",e)}get style(){return this.get("style")}set style(e){this.set("style",e)}get explode(){return this.get("explode")}set explode(e){this.set("explode",e)}get allowedReserved(){return this.get("allowedReserved")}set allowedReserved(e){this.set("allowedReserved",e)}}const ra=na;class oa extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="example"}get summary(){return this.get("summary")}set summary(e){this.set("summary",e)}get description(){return this.get("description")}set description(e){this.set("description",e)}get value(){return this.get("value")}set value(e){this.set("value",e)}get externalValue(){return this.get("externalValue")}set externalValue(e){this.set("externalValue",e)}}const sa=oa;class ia extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="externalDocumentation"}get description(){return this.get("description")}set description(e){this.set("description",e)}get url(){return this.get("url")}set url(e){this.set("url",e)}}const aa=ia;class la extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="header"}get required(){return this.hasKey("required")?this.get("required"):new Pt.hh(!1)}set required(e){this.set("required",e)}get deprecated(){return this.hasKey("deprecated")?this.get("deprecated"):new Pt.hh(!1)}set deprecated(e){this.set("deprecated",e)}get allowEmptyValue(){return this.get("allowEmptyValue")}set allowEmptyValue(e){this.set("allowEmptyValue",e)}get style(){return this.get("style")}set style(e){this.set("style",e)}get explode(){return this.get("explode")}set explode(e){this.set("explode",e)}get allowReserved(){return this.get("allowReserved")}set allowReserved(e){this.set("allowReserved",e)}get schema(){return this.get("schema")}set schema(e){this.set("schema",e)}get example(){return this.get("example")}set example(e){this.set("example",e)}get examples(){return this.get("examples")}set examples(e){this.set("examples",e)}get contentProp(){return this.get("content")}set contentProp(e){this.set("content",e)}}Object.defineProperty(la.prototype,"description",{get(){return this.get("description")},set(e){this.set("description",e)},enumerable:!0});const ca=la;class ua extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="info",this.classes.push("info")}get title(){return this.get("title")}set title(e){this.set("title",e)}get description(){return this.get("description")}set description(e){this.set("description",e)}get termsOfService(){return this.get("termsOfService")}set termsOfService(e){this.set("termsOfService",e)}get contact(){return this.get("contact")}set contact(e){this.set("contact",e)}get license(){return this.get("license")}set license(e){this.set("license",e)}get version(){return this.get("version")}set version(e){this.set("version",e)}}const pa=ua;class ha extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="license"}get name(){return this.get("name")}set name(e){this.set("name",e)}get url(){return this.get("url")}set url(e){this.set("url",e)}}const fa=ha;class da extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="link"}get operationRef(){return this.get("operationRef")}set operationRef(e){this.set("operationRef",e)}get operationId(){return this.get("operationId")}set operationId(e){this.set("operationId",e)}get operation(){var e,t;return ms(this.operationRef)?null===(e=this.operationRef)||void 0===e?void 0:e.meta.get("operation"):ms(this.operationId)?null===(t=this.operationId)||void 0===t?void 0:t.meta.get("operation"):void 0}set operation(e){this.set("operation",e)}get parameters(){return this.get("parameters")}set parameters(e){this.set("parameters",e)}get requestBody(){return this.get("requestBody")}set requestBody(e){this.set("requestBody",e)}get description(){return this.get("description")}set description(e){this.set("description",e)}get server(){return this.get("server")}set server(e){this.set("server",e)}}const ma=da;class ga extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="mediaType"}get schema(){return this.get("schema")}set schema(e){this.set("schema",e)}get example(){return this.get("example")}set example(e){this.set("example",e)}get examples(){return this.get("examples")}set examples(e){this.set("examples",e)}get encoding(){return this.get("encoding")}set encoding(e){this.set("encoding",e)}}const ya=ga;class va extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="oAuthFlow"}get authorizationUrl(){return this.get("authorizationUrl")}set authorizationUrl(e){this.set("authorizationUrl",e)}get tokenUrl(){return this.get("tokenUrl")}set tokenUrl(e){this.set("tokenUrl",e)}get refreshUrl(){return this.get("refreshUrl")}set refreshUrl(e){this.set("refreshUrl",e)}get scopes(){return this.get("scopes")}set scopes(e){this.set("scopes",e)}}const ba=va;class wa extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="oAuthFlows"}get implicit(){return this.get("implicit")}set implicit(e){this.set("implicit",e)}get password(){return this.get("password")}set password(e){this.set("password",e)}get clientCredentials(){return this.get("clientCredentials")}set clientCredentials(e){this.set("clientCredentials",e)}get authorizationCode(){return this.get("authorizationCode")}set authorizationCode(e){this.set("authorizationCode",e)}}const Ea=wa;class xa extends Pt.RP{constructor(e,t,n){super(e,t,n),this.element="openapi",this.classes.push("spec-version"),this.classes.push("version")}}const Sa=xa;class _a extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="openApi3_0",this.classes.push("api")}get openapi(){return this.get("openapi")}set openapi(e){this.set("openapi",e)}get info(){return this.get("info")}set info(e){this.set("info",e)}get servers(){return this.get("servers")}set servers(e){this.set("servers",e)}get paths(){return this.get("paths")}set paths(e){this.set("paths",e)}get components(){return this.get("components")}set components(e){this.set("components",e)}get security(){return this.get("security")}set security(e){this.set("security",e)}get tags(){return this.get("tags")}set tags(e){this.set("tags",e)}get externalDocs(){return this.get("externalDocs")}set externalDocs(e){this.set("externalDocs",e)}}const ja=_a;class Oa extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="operation"}get tags(){return this.get("tags")}set tags(e){this.set("tags",e)}get summary(){return this.get("summary")}set summary(e){this.set("summary",e)}get description(){return this.get("description")}set description(e){this.set("description",e)}set externalDocs(e){this.set("externalDocs",e)}get externalDocs(){return this.get("externalDocs")}get operationId(){return this.get("operationId")}set operationId(e){this.set("operationId",e)}get parameters(){return this.get("parameters")}set parameters(e){this.set("parameters",e)}get requestBody(){return this.get("requestBody")}set requestBody(e){this.set("requestBody",e)}get responses(){return this.get("responses")}set responses(e){this.set("responses",e)}get callbacks(){return this.get("callbacks")}set callbacks(e){this.set("callbacks",e)}get deprecated(){return this.hasKey("deprecated")?this.get("deprecated"):new Pt.hh(!1)}set deprecated(e){this.set("deprecated",e)}get security(){return this.get("security")}set security(e){this.set("security",e)}get servers(){return this.get("severs")}set servers(e){this.set("servers",e)}}const ka=Oa;class Aa extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="parameter"}get name(){return this.get("name")}set name(e){this.set("name",e)}get in(){return this.get("in")}set in(e){this.set("in",e)}get required(){return this.hasKey("required")?this.get("required"):new Pt.hh(!1)}set required(e){this.set("required",e)}get deprecated(){return this.hasKey("deprecated")?this.get("deprecated"):new Pt.hh(!1)}set deprecated(e){this.set("deprecated",e)}get allowEmptyValue(){return this.get("allowEmptyValue")}set allowEmptyValue(e){this.set("allowEmptyValue",e)}get style(){return this.get("style")}set style(e){this.set("style",e)}get explode(){return this.get("explode")}set explode(e){this.set("explode",e)}get allowReserved(){return this.get("allowReserved")}set allowReserved(e){this.set("allowReserved",e)}get schema(){return this.get("schema")}set schema(e){this.set("schema",e)}get example(){return this.get("example")}set example(e){this.set("example",e)}get examples(){return this.get("examples")}set examples(e){this.set("examples",e)}get contentProp(){return this.get("content")}set contentProp(e){this.set("content",e)}}Object.defineProperty(Aa.prototype,"description",{get(){return this.get("description")},set(e){this.set("description",e)},enumerable:!0});const Ca=Aa;class Pa extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="pathItem"}get $ref(){return this.get("$ref")}set $ref(e){this.set("$ref",e)}get summary(){return this.get("summary")}set summary(e){this.set("summary",e)}get description(){return this.get("description")}set description(e){this.set("description",e)}get GET(){return this.get("get")}set GET(e){this.set("GET",e)}get PUT(){return this.get("put")}set PUT(e){this.set("PUT",e)}get POST(){return this.get("post")}set POST(e){this.set("POST",e)}get DELETE(){return this.get("delete")}set DELETE(e){this.set("DELETE",e)}get OPTIONS(){return this.get("options")}set OPTIONS(e){this.set("OPTIONS",e)}get HEAD(){return this.get("head")}set HEAD(e){this.set("HEAD",e)}get PATCH(){return this.get("patch")}set PATCH(e){this.set("PATCH",e)}get TRACE(){return this.get("trace")}set TRACE(e){this.set("TRACE",e)}get servers(){return this.get("servers")}set servers(e){this.set("servers",e)}get parameters(){return this.get("parameters")}set parameters(e){this.set("parameters",e)}}const Na=Pa;class Ia extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="paths"}}const Ta=Ia;class Ra extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="reference",this.classes.push("openapi-reference")}get $ref(){return this.get("$ref")}set $ref(e){this.set("$ref",e)}}const Ma=Ra;class Da extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="requestBody"}get description(){return this.get("description")}set description(e){this.set("description",e)}get contentProp(){return this.get("content")}set contentProp(e){this.set("content",e)}get required(){return this.hasKey("required")?this.get("required"):new Pt.hh(!1)}set required(e){this.set("required",e)}}const Fa=Da;class La extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="response"}get description(){return this.get("description")}set description(e){this.set("description",e)}get headers(){return this.get("headers")}set headers(e){this.set("headers",e)}get contentProp(){return this.get("content")}set contentProp(e){this.set("content",e)}get links(){return this.get("links")}set links(e){this.set("links",e)}}const Ba=La;class $a extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="responses"}get default(){return this.get("default")}set default(e){this.set("default",e)}}const qa=$a;class Ua extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="JSONSchemaDraft4"}get idProp(){return this.get("id")}set idProp(e){this.set("id",e)}get $schema(){return this.get("$schema")}set $schema(e){this.set("idProp",e)}get multipleOf(){return this.get("multipleOf")}set multipleOf(e){this.set("multipleOf",e)}get maximum(){return this.get("maximum")}set maximum(e){this.set("maximum",e)}get exclusiveMaximum(){return this.get("exclusiveMaximum")}set exclusiveMaximum(e){this.set("exclusiveMaximum",e)}get minimum(){return this.get("minimum")}set minimum(e){this.set("minimum",e)}get exclusiveMinimum(){return this.get("exclusiveMinimum")}set exclusiveMinimum(e){this.set("exclusiveMinimum",e)}get maxLength(){return this.get("maxLength")}set maxLength(e){this.set("maxLength",e)}get minLength(){return this.get("minLength")}set minLength(e){this.set("minLength",e)}get pattern(){return this.get("pattern")}set pattern(e){this.set("pattern",e)}get additionalItems(){return this.get("additionalItems")}set additionalItems(e){this.set("additionalItems",e)}get items(){return this.get("items")}set items(e){this.set("items",e)}get maxItems(){return this.get("maxItems")}set maxItems(e){this.set("maxItems",e)}get minItems(){return this.get("minItems")}set minItems(e){this.set("minItems",e)}get uniqueItems(){return this.get("uniqueItems")}set uniqueItems(e){this.set("uniqueItems",e)}get maxProperties(){return this.get("maxProperties")}set maxProperties(e){this.set("maxProperties",e)}get minProperties(){return this.get("minProperties")}set minProperties(e){this.set("minProperties",e)}get required(){return this.get("required")}set required(e){this.set("required",e)}get properties(){return this.get("properties")}set properties(e){this.set("properties",e)}get additionalProperties(){return this.get("additionalProperties")}set additionalProperties(e){this.set("additionalProperties",e)}get patternProperties(){return this.get("patternProperties")}set patternProperties(e){this.set("patternProperties",e)}get dependencies(){return this.get("dependencies")}set dependencies(e){this.set("dependencies",e)}get enum(){return this.get("enum")}set enum(e){this.set("enum",e)}get type(){return this.get("type")}set type(e){this.set("type",e)}get allOf(){return this.get("allOf")}set allOf(e){this.set("allOf",e)}get anyOf(){return this.get("anyOf")}set anyOf(e){this.set("anyOf",e)}get oneOf(){return this.get("oneOf")}set oneOf(e){this.set("oneOf",e)}get not(){return this.get("not")}set not(e){this.set("not",e)}get definitions(){return this.get("definitions")}set definitions(e){this.set("definitions",e)}get title(){return this.get("title")}set title(e){this.set("title",e)}get description(){return this.get("description")}set description(e){this.set("description",e)}get default(){return this.get("default")}set default(e){this.set("default",e)}get format(){return this.get("format")}set format(e){this.set("format",e)}get base(){return this.get("base")}set base(e){this.set("base",e)}get links(){return this.get("links")}set links(e){this.set("links",e)}get media(){return this.get("media")}set media(e){this.set("media",e)}get readOnly(){return this.get("readOnly")}set readOnly(e){this.set("readOnly",e)}}const za=Ua;class Va extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="JSONReference",this.classes.push("json-reference")}get $ref(){return this.get("$ref")}set $ref(e){this.set("$ref",e)}}const Wa=Va;class Ja extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="media"}get binaryEncoding(){return this.get("binaryEncoding")}set binaryEncoding(e){this.set("binaryEncoding",e)}get type(){return this.get("type")}set type(e){this.set("type",e)}}const Ka=Ja;class Ha extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.element="linkDescription"}get href(){return this.get("href")}set href(e){this.set("href",e)}get rel(){return this.get("rel")}set rel(e){this.set("rel",e)}get title(){return this.get("title")}set title(e){this.set("title",e)}get targetSchema(){return this.get("targetSchema")}set targetSchema(e){this.set("targetSchema",e)}get mediaType(){return this.get("mediaType")}set mediaType(e){this.set("mediaType",e)}get method(){return this.get("method")}set method(e){this.set("method",e)}get encType(){return this.get("encType")}set encType(e){this.set("encType",e)}get schema(){return this.get("schema")}set schema(e){this.set("schema",e)}}const Ga=Ha,Za=(e,t)=>{const n=kr(e,t);return po((e=>{if($s(e)&&Hr("$ref",e)&&_o(Xs,"$ref",e)){const t=uo(["$ref"],e),r=qi("#/",t);return uo(r.split("/"),n)}return $s(e)?Za(e,n):e}),e)},Ya=Ys({props:{element:null},methods:{copyMetaAndAttributes(e,t){Cs(e)&&t.meta.set("sourceMap",e.meta.get("sourceMap"))}}}),Xa=Ya,Qa=Ys(Xa,{methods:{enter(e){return this.element=e.clone(),ei}}});const el=Hn($o());function tl(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}const nl=e=>{if(ds(e))return`${e.element.charAt(0).toUpperCase()+e.element.slice(1)}Element`},rl=function(e){for(var t=1;t{if(ms(r)&&n.includes(r.toValue())&&!this.ignoredFields.includes(r.toValue())){const n=this.toRefractedElement([...t,"fixedFields",r.toValue()],e),s=new Pt.c6(r.clone(),n);this.copyMetaAndAttributes(o,s),s.classes.push("fixed-field"),this.element.content.push(s)}else this.ignoredFields.includes(r.toValue())||this.element.content.push(o.clone())})),this.copyMetaAndAttributes(e,this.element),ei}}}),ll=al,cl=Ys(ll,Qa,{props:{specPath:Hn(["document","objects","JSONSchema"])},init(){this.element=new za}}),ul=Qa,pl=Qa,hl=Qa,fl=Qa,dl=Qa,ml=Qa,gl=Qa,yl=Qa,vl=Qa,bl=Qa,wl=Ys({props:{parent:null},init({parent:e=this.parent}){this.parent=e,this.passingOptionsNames=[...this.passingOptionsNames,"parent"]}}),El=e=>bs(e)&&e.hasKey("$ref"),xl=Ys(il,wl,Qa,{methods:{ObjectElement(e){const t=El(e)?["document","objects","JSONReference"]:["document","objects","JSONSchema"];return this.element=this.toRefractedElement(t,e),ei},ArrayElement(e){return this.element=new Pt.ON,this.element.classes.push("json-schema-items"),e.forEach((e=>{const t=El(e)?["document","objects","JSONReference"]:["document","objects","JSONSchema"],n=this.toRefractedElement(t,e);this.element.push(n)})),this.copyMetaAndAttributes(e,this.element),ei}}}),Sl=Qa,_l=Qa,jl=Qa,Ol=Qa,kl=Qa,Al=Ys(Qa,{methods:{ArrayElement(e){return this.element=e.clone(),this.element.classes.push("json-schema-required"),ei}}});const Cl=pr(Zt(1,cr(Ts,Ur(Ms,ts))));const Pl=pr(so);const Nl=Kn([Xs,Cl,Pl]),Il=Ys(il,{props:{fieldPatternPredicate:Mt,specPath:el,ignoredFields:[]},init({specPath:e=this.specPath,ignoredFields:t=this.ignoredFields}={}){this.specPath=e,this.ignoredFields=t},methods:{ObjectElement(e){return e.forEach(((e,t,n)=>{if(!this.ignoredFields.includes(t.toValue())&&this.fieldPatternPredicate(t.toValue())){const r=this.specPath(e),o=this.toRefractedElement(r,e),s=new Pt.c6(t.clone(),o);this.copyMetaAndAttributes(n,s),s.classes.push("patterned-field"),this.element.content.push(s)}else this.ignoredFields.includes(t.toValue())||this.element.content.push(n.clone())})),this.copyMetaAndAttributes(e,this.element),ei}}}),Tl=Ys(Il,{props:{fieldPatternPredicate:Nl}}),Rl=Ys(Tl,wl,Qa,{props:{specPath:e=>El(e)?["document","objects","JSONReference"]:["document","objects","JSONSchema"]},init(){this.element=new Pt.Sb,this.element.classes.push("json-schema-properties")}}),Ml=Ys(Tl,wl,Qa,{props:{specPath:e=>El(e)?["document","objects","JSONReference"]:["document","objects","JSONSchema"]},init(){this.element=new Pt.Sb,this.element.classes.push("json-schema-patternProperties")}}),Dl=Ys(Tl,wl,Qa,{props:{specPath:e=>El(e)?["document","objects","JSONReference"]:["document","objects","JSONSchema"]},init(){this.element=new Pt.Sb,this.element.classes.push("json-schema-dependencies")}}),Fl=Ys(Qa,{methods:{ArrayElement(e){return this.element=e.clone(),this.element.classes.push("json-schema-enum"),ei}}}),Ll=Ys(Qa,{methods:{StringElement(e){return this.element=e.clone(),this.element.classes.push("json-schema-type"),ei},ArrayElement(e){return this.element=e.clone(),this.element.classes.push("json-schema-type"),ei}}}),Bl=Ys(il,wl,Qa,{init(){this.element=new Pt.ON,this.element.classes.push("json-schema-allOf")},methods:{ArrayElement(e){return e.forEach((e=>{const t=El(e)?["document","objects","JSONReference"]:["document","objects","JSONSchema"],n=this.toRefractedElement(t,e);this.element.push(n)})),this.copyMetaAndAttributes(e,this.element),ei}}}),$l=Ys(il,wl,Qa,{init(){this.element=new Pt.ON,this.element.classes.push("json-schema-anyOf")},methods:{ArrayElement(e){return e.forEach((e=>{const t=El(e)?["document","objects","JSONReference"]:["document","objects","JSONSchema"],n=this.toRefractedElement(t,e);this.element.push(n)})),this.copyMetaAndAttributes(e,this.element),ei}}}),ql=Ys(il,wl,Qa,{init(){this.element=new Pt.ON,this.element.classes.push("json-schema-oneOf")},methods:{ArrayElement(e){return e.forEach((e=>{const t=El(e)?["document","objects","JSONReference"]:["document","objects","JSONSchema"],n=this.toRefractedElement(t,e);this.element.push(n)})),this.copyMetaAndAttributes(e,this.element),ei}}}),Ul=Ys(Tl,wl,Qa,{props:{specPath:e=>El(e)?["document","objects","JSONReference"]:["document","objects","JSONSchema"]},init(){this.element=new Pt.Sb,this.element.classes.push("json-schema-definitions")}}),zl=Qa,Vl=Qa,Wl=Qa,Jl=Qa,Kl=Qa,Hl=Ys(il,wl,Qa,{init(){this.element=new Pt.ON,this.element.classes.push("json-schema-links")},methods:{ArrayElement(e){return e.forEach((e=>{const t=this.toRefractedElement(["document","objects","LinkDescription"],e);this.element.push(t)})),this.copyMetaAndAttributes(e,this.element),ei}}}),Gl=Qa,Zl=Ys(ll,Qa,{props:{specPath:Hn(["document","objects","JSONReference"])},init(){this.element=new Wa},methods:{ObjectElement(e){const t=ll.compose.methods.ObjectElement.call(this,e);return ms(this.element.$ref)&&this.element.classes.push("reference-element"),t}}}),Yl=Ys(Qa,{methods:{StringElement(e){return this.element=e.clone(),this.element.classes.push("reference-value"),ei}}});const Xl=pr(rr);const Ql=cr(rs,Pl);function ec(e){return function(e){if(Array.isArray(e))return tc(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(!e)return;if("string"==typeof e)return tc(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return tc(e,t)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function tc(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);nt.length}))),Zr,Tn("length")),rc=Or((function(e,t,n){var r=n.apply(void 0,ec(e));return Xl(r)?Ao(r):t}));const oc=to(Ql,(function(e){var t=nc(e);return Zt(t,(function(){for(var t=arguments.length,n=new Array(t),r=0;rto(e,Hn(t),$o))),n=oc(t)(e);return this.element=this.toRefractedElement(n,e),ei}}}),ic=Ys(sc,{props:{alternator:[{predicate:El,specPath:["document","objects","JSONReference"]},{predicate:Dt,specPath:["document","objects","JSONSchema"]}]}}),ac={visitors:{value:Qa,JSONSchemaOrJSONReferenceVisitor:ic,document:{objects:{JSONSchema:{$visitor:cl,fixedFields:{id:ul,$schema:pl,multipleOf:hl,maximum:fl,exclusiveMaximum:dl,minimum:ml,exclusiveMinimum:gl,maxLength:yl,minLength:vl,pattern:bl,additionalItems:ic,items:xl,maxItems:Sl,minItems:_l,uniqueItems:jl,maxProperties:Ol,minProperties:kl,required:Al,properties:Rl,additionalProperties:ic,patternProperties:Ml,dependencies:Dl,enum:Fl,type:Ll,allOf:Bl,anyOf:$l,oneOf:ql,not:ic,definitions:Ul,title:zl,description:Vl,default:Wl,format:Jl,base:Kl,links:Hl,media:{$ref:"#/visitors/document/objects/Media"},readOnly:Gl}},JSONReference:{$visitor:Zl,fixedFields:{$ref:Yl}},Media:{$visitor:Ys(ll,Qa,{props:{specPath:Hn(["document","objects","Media"])},init(){this.element=new Ka}}),fixedFields:{binaryEncoding:Qa,type:Qa}},LinkDescription:{$visitor:Ys(ll,Qa,{props:{specPath:Hn(["document","objects","LinkDescription"])},init(){this.element=new Ga}}),fixedFields:{href:Qa,rel:Qa,title:Qa,targetSchema:ic,mediaType:Qa,method:Qa,encType:Qa,schema:ic}}}}}},lc=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof za||e(r)&&t("JSONSchemaDraft4",r)&&n("object",r))),cc=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Wa||e(r)&&t("JSONReference",r)&&n("object",r))),uc=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Ka||e(r)&&t("media",r)&&n("object",r))),pc=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Ga||e(r)&&t("linkDescription",r)&&n("object",r))),hc={namespace:e=>{const{base:t}=e;return t.register("jSONSchemaDraft4",za),t.register("jSONReference",Wa),t.register("media",Ka),t.register("linkDescription",Ga),t}};function fc(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function dc(e){for(var t=1;t{const e=zs(hc);return{predicates:dc(dc({},i),{},{isStringElement:ms}),namespace:e}};function gc(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}const yc=(e,{specPath:t=["visitors","document","objects","JSONSchema","$visitor"],plugins:n=[],specificationObj:r=ac}={})=>{const o=(0,Pt.Qc)(e),s=Za(r),i=is(t,[],s);return fi(o,i,{state:{specObj:s}}),di(i.element,n,{toolboxCreator:mc,visitorOptions:{keyMap:rl,nodeTypeGetter:nl}})},vc=e=>(t,n={})=>yc(t,function(e){for(var t=1;t{if(ds(e))return`${e.element.charAt(0).toUpperCase()+e.element.slice(1)}Element`},Dc=function(e){for(var t=1;tbs(e)&&e.hasKey("openapi")&&e.hasKey("info"),qc=e=>bs(e)&&e.hasKey("name")&&e.hasKey("in"),Uc=e=>bs(e)&&e.hasKey("$ref"),zc=e=>bs(e)&&e.hasKey("content"),Vc=e=>bs(e)&&e.hasKey("description"),Wc=bs,Jc=bs,Kc=e=>ms(e.key)&&To("x-",e.key.toValue()),Hc=Ys(Bc,{props:{specPath:el,ignoredFields:[],canSupportSpecificationExtensions:!0,specificationExtensionPredicate:Kc},init({specPath:e=this.specPath,ignoredFields:t=this.ignoredFields,canSupportSpecificationExtensions:n=this.canSupportSpecificationExtensions,specificationExtensionPredicate:r=this.specificationExtensionPredicate}={}){this.specPath=e,this.ignoredFields=t,this.canSupportSpecificationExtensions=n,this.specificationExtensionPredicate=r},methods:{ObjectElement(e){const t=this.specPath(e),n=this.retrieveFixedFields(t);return e.forEach(((e,r,o)=>{if(ms(r)&&n.includes(r.toValue())&&!this.ignoredFields.includes(r.toValue())){const n=this.toRefractedElement([...t,"fixedFields",r.toValue()],e),s=new Pt.c6(r.clone(),n);this.copyMetaAndAttributes(o,s),s.classes.push("fixed-field"),this.element.content.push(s)}else if(this.canSupportSpecificationExtensions&&this.specificationExtensionPredicate(o)){const e=this.toRefractedElement(["document","extension"],o);this.element.content.push(e)}else this.ignoredFields.includes(r.toValue())||this.element.content.push(o.clone())})),this.copyMetaAndAttributes(e,this.element),ei}}}),Gc=Hc,Zc=Ys(Tc,{methods:{enter(e){return this.element=e.clone(),ei}}}),Yc=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","OpenApi"]),canSupportSpecificationExtensions:!0},init(){this.element=new ja},methods:{ObjectElement(e){return this.unrefractedElement=e,Gc.compose.methods.ObjectElement.call(this,e)}}}),Xc=Ys(Bc,Zc,{methods:{StringElement(e){const t=new Sa(e.toValue());return this.copyMetaAndAttributes(e,t),this.element=t,ei}}}),Qc=Ys(Bc,{methods:{MemberElement(e){return this.element=e.clone(),this.element.classes.push("specification-extension"),ei}}}),eu=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","Info"]),canSupportSpecificationExtensions:!0},init(){this.element=new pa}}),tu=Zc,nu=Zc,ru=Zc,ou=Ys(Zc,{methods:{StringElement(e){return this.element=e.clone(),this.element.classes.push("api-version"),this.element.classes.push("version"),ei}}}),su=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","Contact"]),canSupportSpecificationExtensions:!0},init(){this.element=new Qi}}),iu=Zc,au=Zc,lu=Zc,cu=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","License"]),canSupportSpecificationExtensions:!0},init(){this.element=new fa}}),uu=Zc,pu=Zc,hu=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","Link"]),canSupportSpecificationExtensions:!0},init(){this.element=new ma},methods:{ObjectElement(e){const t=Gc.compose.methods.ObjectElement.call(this,e);return(ms(this.element.operationId)||ms(this.element.operationRef))&&this.element.classes.push("reference-element"),t}}}),fu=Ys(Zc,{methods:{StringElement(e){return this.element=e.clone(),this.element.classes.push("reference-value"),ei}}}),du=Ys(Zc,{methods:{StringElement(e){return this.element=e.clone(),this.element.classes.push("reference-value"),ei}}}),mu=Ys(Bc,{props:{fieldPatternPredicate:Mt,specPath:el,ignoredFields:[],canSupportSpecificationExtensions:!1,specificationExtensionPredicate:Kc},init({specPath:e=this.specPath,ignoredFields:t=this.ignoredFields,canSupportSpecificationExtensions:n=this.canSupportSpecificationExtensions,specificationExtensionPredicate:r=this.specificationExtensionPredicate}={}){this.specPath=e,this.ignoredFields=t,this.canSupportSpecificationExtensions=n,this.specificationExtensionPredicate=r},methods:{ObjectElement(e){return e.forEach(((e,t,n)=>{if(this.canSupportSpecificationExtensions&&this.specificationExtensionPredicate(n)){const e=this.toRefractedElement(["document","extension"],n);this.element.content.push(e)}else if(!this.ignoredFields.includes(t.toValue())&&this.fieldPatternPredicate(t.toValue())){const r=this.specPath(e),o=this.toRefractedElement(r,e),s=new Pt.c6(t.clone(),o);this.copyMetaAndAttributes(n,s),s.classes.push("patterned-field"),this.element.content.push(s)}else this.ignoredFields.includes(t.toValue())||this.element.content.push(n.clone())})),this.copyMetaAndAttributes(e,this.element),ei}}}),gu=mu,yu=Ys(gu,{props:{fieldPatternPredicate:Nl}});class vu extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(vu.primaryClass)}}Xo(vu,"primaryClass","link-parameters");const bu=vu,wu=Ys(yu,Zc,{props:{specPath:Hn(["value"])},init(){this.element=new bu}}),Eu=Zc,xu=Zc,Su=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","Server"]),canSupportSpecificationExtensions:!0},init(){this.element=new jc}}),_u=Ys(Zc,{methods:{StringElement(e){return this.element=e.clone(),this.element.classes.push("server-url"),ei}}}),ju=Zc;class Ou extends Pt.ON{constructor(e,t,n){super(e,t,n),this.classes.push(Ou.primaryClass)}}Xo(Ou,"primaryClass","servers");const ku=Ou,Au=Ys(Bc,Zc,{init(){this.element=new ku},methods:{ArrayElement(e){return e.forEach((e=>{const t=Wc(e)?["document","objects","Server"]:["value"],n=this.toRefractedElement(t,e);this.element.push(n)})),this.copyMetaAndAttributes(e,this.element),ei}}}),Cu=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","ServerVariable"]),canSupportSpecificationExtensions:!0},init(){this.element=new kc}}),Pu=Zc,Nu=Zc,Iu=Zc;class Tu extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(Tu.primaryClass)}}Xo(Tu,"primaryClass","server-variables");const Ru=Tu,Mu=Ys(yu,Zc,{props:{specPath:Hn(["document","objects","ServerVariable"])},init(){this.element=new Ru}}),Du=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","MediaType"]),canSupportSpecificationExtensions:!0},init(){this.element=new ya}}),Fu=Ys(Bc,{props:{alternator:[]},methods:{enter(e){const t=this.alternator.map((({predicate:e,specPath:t})=>to(e,Hn(t),$o))),n=oc(t)(e);return this.element=this.toRefractedElement(n,e),ei}}}),Lu=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Gi||e(r)&&t("callback",r)&&n("object",r))),Bu=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Yi||e(r)&&t("components",r)&&n("object",r))),$u=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Qi||e(r)&&t("contact",r)&&n("object",r))),qu=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof sa||e(r)&&t("example",r)&&n("object",r))),Uu=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof aa||e(r)&&t("externalDocumentation",r)&&n("object",r))),zu=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof ca||e(r)&&t("header",r)&&n("object",r))),Vu=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof pa||e(r)&&t("info",r)&&n("object",r))),Wu=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof fa||e(r)&&t("license",r)&&n("object",r))),Ju=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof ma||e(r)&&t("link",r)&&n("object",r))),Ku=e=>{if(!Ju(e))return!1;if(!ms(e.operationRef))return!1;const t=e.operationRef.toValue();return"string"==typeof t&&t.length>0&&!t.startsWith("#")},Hu=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Sa||e(r)&&t("openapi",r)&&n("string",r))),Gu=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n,hasClass:r})=>o=>o instanceof ja||e(o)&&t("openApi3_0",o)&&n("object",o)&&r("api",o))),Zu=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof ka||e(r)&&t("operation",r)&&n("object",r))),Yu=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Ca||e(r)&&t("parameter",r)&&n("object",r))),Xu=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Na||e(r)&&t("pathItem",r)&&n("object",r))),Qu=e=>{if(!Xu(e))return!1;if(!ms(e.$ref))return!1;const t=e.$ref.toValue();return"string"==typeof t&&t.length>0&&!t.startsWith("#")},ep=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Ta||e(r)&&t("paths",r)&&n("object",r))),tp=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Ma||e(r)&&t("reference",r)&&n("object",r))),np=e=>{if(!tp(e))return!1;if(!ms(e.$ref))return!1;const t=e.$ref.toValue();return"string"==typeof t&&t.length>0&&!t.startsWith("#")},rp=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Fa||e(r)&&t("requestBody",r)&&n("object",r))),op=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Ba||e(r)&&t("response",r)&&n("object",r))),sp=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof qa||e(r)&&t("responses",r)&&n("object",r))),ip=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof bc||e(r)&&t("schema",r)&&n("object",r))),ap=e=>vs(e)&&e.classes.includes("boolean-json-schema"),lp=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Ec||e(r)&&t("securityRequirement",r)&&n("object",r))),cp=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof jc||e(r)&&t("server",r)&&n("object",r))),up=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof kc||e(r)&&t("serverVariable",r)&&n("object",r))),pp=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof ya||e(r)&&t("mediaType",r)&&n("object",r))),hp=Ys(Fu,Zc,{props:{alternator:[{predicate:Uc,specPath:["document","objects","Reference"]},{predicate:Dt,specPath:["document","objects","Schema"]}]},methods:{ObjectElement(e){const t=Fu.compose.methods.enter.call(this,e);return tp(this.element)&&this.element.setMetaProperty("referenced-element","schema"),t}}}),fp=Zc,dp=Ys(yu,Zc,{props:{specPath:e=>Uc(e)?["document","objects","Reference"]:["document","objects","Example"],canSupportSpecificationExtensions:!0},init(){this.element=new Pt.Sb,this.element.classes.push("examples")},methods:{ObjectElement(e){const t=yu.compose.methods.ObjectElement.call(this,e);return this.element.filter(tp).forEach((e=>{e.setMetaProperty("referenced-element","example")})),t}}});class mp extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(mp.primaryClass),this.classes.push("examples")}}Xo(mp,"primaryClass","media-type-examples");const gp=mp,yp=Ys(dp,{init(){this.element=new gp}});class vp extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(vp.primaryClass)}}Xo(vp,"primaryClass","media-type-encoding");const bp=vp,wp=Ys(yu,Zc,{props:{specPath:Hn(["document","objects","Encoding"])},init(){this.element=new bp}}),Ep=Ys(yu,Zc,{props:{specPath:Hn(["value"])},init(){this.element=new Ec}});class xp extends Pt.ON{constructor(e,t,n){super(e,t,n),this.classes.push(xp.primaryClass)}}Xo(xp,"primaryClass","security");const Sp=xp,_p=Ys(Bc,Zc,{init(){this.element=new Sp},methods:{ArrayElement(e){return e.forEach((e=>{if(bs(e)){const t=this.toRefractedElement(["document","objects","SecurityRequirement"],e);this.element.push(t)}else this.element.push(e.clone())})),this.copyMetaAndAttributes(e,this.element),ei}}}),jp=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","Components"]),canSupportSpecificationExtensions:!0},init(){this.element=new Yi}}),Op=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","Tag"]),canSupportSpecificationExtensions:!0},init(){this.element=new Cc}}),kp=Zc,Ap=Zc,Cp=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","Reference"]),canSupportSpecificationExtensions:!1},init(){this.element=new Ma},methods:{ObjectElement(e){const t=Gc.compose.methods.ObjectElement.call(this,e);return ms(this.element.$ref)&&this.element.classes.push("reference-element"),t}}}),Pp=Ys(Zc,{methods:{StringElement(e){return this.element=e.clone(),this.element.classes.push("reference-value"),ei}}}),Np=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","Parameter"]),canSupportSpecificationExtensions:!0},init(){this.element=new Ca},methods:{ObjectElement(e){const t=Gc.compose.methods.ObjectElement.call(this,e);return bs(this.element.contentProp)&&this.element.contentProp.filter(pp).forEach(((e,t)=>{e.setMetaProperty("media-type",t.toValue())})),t}}}),Ip=Zc,Tp=Zc,Rp=Zc,Mp=Zc,Dp=Zc,Fp=Zc,Lp=Zc,Bp=Zc,$p=Zc,qp=Ys(Fu,Zc,{props:{alternator:[{predicate:Uc,specPath:["document","objects","Reference"]},{predicate:Dt,specPath:["document","objects","Schema"]}]},methods:{ObjectElement(e){const t=Fu.compose.methods.enter.call(this,e);return tp(this.element)&&this.element.setMetaProperty("referenced-element","schema"),t}}}),Up=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","Header"]),canSupportSpecificationExtensions:!0},init(){this.element=new ca}}),zp=Zc,Vp=Zc,Wp=Zc,Jp=Zc,Kp=Zc,Hp=Zc,Gp=Zc,Zp=Ys(Fu,Zc,{props:{alternator:[{predicate:Uc,specPath:["document","objects","Reference"]},{predicate:Dt,specPath:["document","objects","Schema"]}]},methods:{ObjectElement(e){const t=Fu.compose.methods.enter.call(this,e);return tp(this.element)&&this.element.setMetaProperty("referenced-element","schema"),t}}}),Yp=Zc;class Xp extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(Xp.primaryClass),this.classes.push("examples")}}Xo(Xp,"primaryClass","header-examples");const Qp=Xp,eh=Ys(dp,{init(){this.element=new Qp}}),th=Ys(yu,Zc,{props:{specPath:Hn(["document","objects","MediaType"])},init(){this.element=new Pt.Sb,this.element.classes.push("content")}});class nh extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(nh.primaryClass),this.classes.push("content")}}Xo(nh,"primaryClass","header-content");const rh=nh,oh=Ys(th,{init(){this.element=new rh}}),sh=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","Schema"]),canSupportSpecificationExtensions:!0},init(){this.element=new bc}}),{items:ih}=ac.visitors.document.objects.JSONSchema.fixedFields,ah=Ys(ih,{methods:{ObjectElement(e){const t=ih.compose.methods.ObjectElement.call(this,e);return tp(this.element)&&this.element.setMetaProperty("referenced-element","schema"),t},ArrayElement(e){return this.element=e.clone(),ei}}}),{properties:lh}=ac.visitors.document.objects.JSONSchema.fixedFields,ch=Ys(lh,{methods:{ObjectElement(e){const t=lh.compose.methods.ObjectElement.call(this,e);return this.element.filter(tp).forEach((e=>{e.setMetaProperty("referenced-element","schema")})),t}}}),{type:uh}=ac.visitors.document.objects.JSONSchema.fixedFields,ph=Ys(uh,{methods:{ArrayElement(e){return this.element=e.clone(),ei}}}),hh=Zc,fh=Zc,dh=Zc,mh=Zc,{JSONSchemaOrJSONReferenceVisitor:gh}=ac.visitors,yh=Ys(gh,{methods:{ObjectElement(e){const t=gh.compose.methods.enter.call(this,e);return tp(this.element)&&this.element.setMetaProperty("referenced-element","schema"),t}}}),vh=Object.fromEntries(Object.entries(ac.visitors.document.objects.JSONSchema.fixedFields).map((([e,t])=>t===ac.visitors.JSONSchemaOrJSONReferenceVisitor?[e,yh]:[e,t]))),bh=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","Discriminator"]),canSupportSpecificationExtensions:!1},init(){this.element=new ta}}),wh=Zc;class Eh extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(Eh.primaryClass)}}Xo(Eh,"primaryClass","discriminator-mapping");const xh=Eh,Sh=Ys(yu,Zc,{props:{specPath:Hn(["value"])},init(){this.element=new xh}}),_h=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","XML"]),canSupportSpecificationExtensions:!0},init(){this.element=new Nc}}),jh=Zc,Oh=Zc,kh=Zc,Ah=Zc,Ch=Zc,Ph=Zc;class Nh extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(Nh.primaryClass),this.classes.push("examples")}}Xo(Nh,"primaryClass","parameter-examples");const Ih=Nh,Th=Ys(dp,{init(){this.element=new Ih}});class Rh extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(Rh.primaryClass),this.classes.push("content")}}Xo(Rh,"primaryClass","parameter-content");const Mh=Rh,Dh=Ys(th,{init(){this.element=new Mh}});class Fh extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(Fh.primaryClass)}}Xo(Fh,"primaryClass","components-schemas");const Lh=Fh,Bh=Ys(yu,Zc,{props:{specPath:e=>Uc(e)?["document","objects","Reference"]:["document","objects","Schema"]},init(){this.element=new Lh},methods:{ObjectElement(e){const t=yu.compose.methods.ObjectElement.call(this,e);return this.element.filter(tp).forEach((e=>{e.setMetaProperty("referenced-element","schema")})),t}}});class $h extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push($h.primaryClass)}}Xo($h,"primaryClass","components-responses");const qh=$h,Uh=Ys(yu,Zc,{props:{specPath:e=>Uc(e)?["document","objects","Reference"]:["document","objects","Response"]},init(){this.element=new qh},methods:{ObjectElement(e){const t=yu.compose.methods.ObjectElement.call(this,e);return this.element.filter(tp).forEach((e=>{e.setMetaProperty("referenced-element","response")})),this.element.filter(op).forEach(((e,t)=>{e.setMetaProperty("http-status-code",t.toValue())})),t}}}),zh=Uh;class Vh extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(Vh.primaryClass),this.classes.push("parameters")}}Xo(Vh,"primaryClass","components-parameters");const Wh=Vh,Jh=Ys(yu,Zc,{props:{specPath:e=>Uc(e)?["document","objects","Reference"]:["document","objects","Parameter"]},init(){this.element=new Wh},methods:{ObjectElement(e){const t=yu.compose.methods.ObjectElement.call(this,e);return this.element.filter(tp).forEach((e=>{e.setMetaProperty("referenced-element","parameter")})),t}}});class Kh extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(Kh.primaryClass),this.classes.push("examples")}}Xo(Kh,"primaryClass","components-examples");const Hh=Kh,Gh=Ys(yu,Zc,{props:{specPath:e=>Uc(e)?["document","objects","Reference"]:["document","objects","Example"]},init(){this.element=new Hh},methods:{ObjectElement(e){const t=yu.compose.methods.ObjectElement.call(this,e);return this.element.filter(tp).forEach((e=>{e.setMetaProperty("referenced-element","example")})),t}}});class Zh extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(Zh.primaryClass)}}Xo(Zh,"primaryClass","components-request-bodies");const Yh=Zh,Xh=Ys(yu,Zc,{props:{specPath:e=>Uc(e)?["document","objects","Reference"]:["document","objects","RequestBody"]},init(){this.element=new Yh},methods:{ObjectElement(e){const t=yu.compose.methods.ObjectElement.call(this,e);return this.element.filter(tp).forEach((e=>{e.setMetaProperty("referenced-element","requestBody")})),t}}});class Qh extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(Qh.primaryClass)}}Xo(Qh,"primaryClass","components-headers");const ef=Qh,tf=Ys(yu,Zc,{props:{specPath:e=>Uc(e)?["document","objects","Reference"]:["document","objects","Header"]},init(){this.element=new ef},methods:{ObjectElement(e){const t=yu.compose.methods.ObjectElement.call(this,e);return this.element.filter(tp).forEach((e=>{e.setMetaProperty("referenced-element","header")})),this.element.filter(zu).forEach(((e,t)=>{e.setMetaProperty("header-name",t.toValue())})),t}}}),nf=tf;class rf extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(rf.primaryClass)}}Xo(rf,"primaryClass","components-security-schemes");const of=rf,sf=Ys(yu,Zc,{props:{specPath:e=>Uc(e)?["document","objects","Reference"]:["document","objects","SecurityScheme"]},init(){this.element=new of},methods:{ObjectElement(e){const t=yu.compose.methods.ObjectElement.call(this,e);return this.element.filter(tp).forEach((e=>{e.setMetaProperty("referenced-element","securityScheme")})),t}}});class af extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(af.primaryClass)}}Xo(af,"primaryClass","components-links");const lf=af,cf=Ys(yu,Zc,{props:{specPath:e=>Uc(e)?["document","objects","Reference"]:["document","objects","Link"]},init(){this.element=new lf},methods:{ObjectElement(e){const t=yu.compose.methods.ObjectElement.call(this,e);return this.element.filter(tp).forEach((e=>{e.setMetaProperty("referenced-element","link")})),t}}});class uf extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(uf.primaryClass)}}Xo(uf,"primaryClass","components-callbacks");const pf=uf,hf=Ys(yu,Zc,{props:{specPath:e=>Uc(e)?["document","objects","Reference"]:["document","objects","Callback"]},init(){this.element=new pf},methods:{ObjectElement(e){const t=yu.compose.methods.ObjectElement.call(this,e);return this.element.filter(tp).forEach((e=>{e.setMetaProperty("referenced-element","callback")})),t}}}),ff=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","Example"]),canSupportSpecificationExtensions:!0},init(){this.element=new sa},methods:{ObjectElement(e){const t=Gc.compose.methods.ObjectElement.call(this,e);return ms(this.element.externalValue)&&this.element.classes.push("reference-element"),t}}}),df=Zc,mf=Zc,gf=Zc,yf=Ys(Zc,{methods:{StringElement(e){return this.element=e.clone(),this.element.classes.push("reference-value"),ei}}}),vf=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","ExternalDocumentation"]),canSupportSpecificationExtensions:!0},init(){this.element=new aa}}),bf=Zc,wf=Zc,Ef=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","Encoding"]),canSupportSpecificationExtensions:!0},init(){this.element=new ra},methods:{ObjectElement(e){const t=Gc.compose.methods.ObjectElement.call(this,e);return bs(this.element.headers)&&this.element.headers.filter(zu).forEach(((e,t)=>{e.setMetaProperty("header-name",t.toValue())})),t}}}),xf=Zc;class Sf extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(Sf.primaryClass)}}Xo(Sf,"primaryClass","encoding-headers");const _f=Sf,jf=Ys(yu,Zc,{props:{specPath:e=>Uc(e)?["document","objects","Reference"]:["document","objects","Header"]},init(){this.element=new _f},methods:{ObjectElement(e){const t=yu.compose.methods.ObjectElement.call(this,e);return this.element.filter(tp).forEach((e=>{e.setMetaProperty("referenced-element","header")})),this.element.forEach(((e,t)=>{if(!zu(e))return;const n=t.toValue();e.setMetaProperty("headerName",n)})),t}}}),Of=jf,kf=Zc,Af=Zc,Cf=Zc,Pf=Ys(gu,Zc,{props:{fieldPatternPredicate:Ro(/^\/(?.*)$/),specPath:Hn(["document","objects","PathItem"]),canSupportSpecificationExtensions:!0},init(){this.element=new Ta},methods:{ObjectElement(e){const t=gu.compose.methods.ObjectElement.call(this,e);return this.element.filter(Xu).forEach(((e,t)=>{e.setMetaProperty("path",t.clone())})),t}}}),Nf=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","RequestBody"])},init(){this.element=new Fa},methods:{ObjectElement(e){const t=Gc.compose.methods.ObjectElement.call(this,e);return bs(this.element.contentProp)&&this.element.contentProp.filter(pp).forEach(((e,t)=>{e.setMetaProperty("media-type",t.toValue())})),t}}}),If=Zc;class Tf extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(Tf.primaryClass),this.classes.push("content")}}Xo(Tf,"primaryClass","request-body-content");const Rf=Tf,Mf=Ys(th,{init(){this.element=new Rf}}),Df=Zc,Ff=Ys(gu,Zc,{props:{fieldPatternPredicate:Ro(/{(?.*)}/),specPath:Hn(["document","objects","PathItem"]),canSupportSpecificationExtensions:!0},init(){this.element=new Gi},methods:{ObjectElement(e){const t=yu.compose.methods.ObjectElement.call(this,e);return this.element.filter(Xu).forEach(((e,t)=>{e.setMetaProperty("runtime-expression",t.toValue())})),t}}}),Lf=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","Response"])},init(){this.element=new Ba},methods:{ObjectElement(e){const t=Gc.compose.methods.ObjectElement.call(this,e);return bs(this.element.contentProp)&&this.element.contentProp.filter(pp).forEach(((e,t)=>{e.setMetaProperty("media-type",t.toValue())})),bs(this.element.headers)&&this.element.headers.filter(zu).forEach(((e,t)=>{e.setMetaProperty("header-name",t.toValue())})),t}}}),Bf=Zc;class $f extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push($f.primaryClass)}}Xo($f,"primaryClass","response-headers");const qf=$f,Uf=Ys(yu,Zc,{props:{specPath:e=>Uc(e)?["document","objects","Reference"]:["document","objects","Header"]},init(){this.element=new qf},methods:{ObjectElement(e){const t=yu.compose.methods.ObjectElement.call(this,e);return this.element.filter(tp).forEach((e=>{e.setMetaProperty("referenced-element","header")})),this.element.forEach(((e,t)=>{if(!zu(e))return;const n=t.toValue();e.setMetaProperty("header-name",n)})),t}}}),zf=Uf;class Vf extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(Vf.primaryClass),this.classes.push("content")}}Xo(Vf,"primaryClass","response-content");const Wf=Vf,Jf=Ys(th,{init(){this.element=new Wf}});class Kf extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(Kf.primaryClass)}}Xo(Kf,"primaryClass","response-links");const Hf=Kf,Gf=Ys(yu,Zc,{props:{specPath:e=>Uc(e)?["document","objects","Reference"]:["document","objects","Link"]},init(){this.element=new Hf},methods:{ObjectElement(e){const t=yu.compose.methods.ObjectElement.call(this,e);return this.element.filter(tp).forEach((e=>{e.setMetaProperty("referenced-element","link")})),t}}}),Zf=Ys(Gc,gu,{props:{specPathFixedFields:el,specPathPatternedFields:el},methods:{ObjectElement(e){const{specPath:t,ignoredFields:n}=this;try{this.specPath=this.specPathFixedFields;const t=this.retrieveFixedFields(this.specPath(e));this.ignoredFields=[...n,...Pr(e.keys(),t)],Gc.compose.methods.ObjectElement.call(this,e),this.specPath=this.specPathPatternedFields,this.ignoredFields=t,gu.compose.methods.ObjectElement.call(this,e)}catch(e){throw this.specPath=t,e}return ei}}}),Yf=Ys(Zf,Zc,{props:{specPathFixedFields:Hn(["document","objects","Responses"]),specPathPatternedFields:e=>Uc(e)?["document","objects","Reference"]:["document","objects","Response"],fieldPatternPredicate:Ro(new RegExp(`^(1XX|2XX|3XX|4XX|5XX|${ko(100,600).join("|")})$`)),canSupportSpecificationExtensions:!0},init(){this.element=new qa},methods:{ObjectElement(e){const t=Zf.compose.methods.ObjectElement.call(this,e);return this.element.filter(tp).forEach((e=>{e.setMetaProperty("referenced-element","response")})),this.element.filter(op).forEach(((e,t)=>{const n=t.clone();this.fieldPatternPredicate(n.toValue())&&e.setMetaProperty("http-status-code",n)})),t}}}),Xf=Yf,Qf=Ys(Fu,Zc,{props:{alternator:[{predicate:Uc,specPath:["document","objects","Reference"]},{predicate:Dt,specPath:["document","objects","Response"]}]},methods:{ObjectElement(e){const t=Fu.compose.methods.enter.call(this,e);return tp(this.element)?this.element.setMetaProperty("referenced-element","response"):op(this.element)&&this.element.setMetaProperty("http-status-code","default"),t}}}),ed=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","Operation"])},init(){this.element=new ka}});class td extends Pt.ON{constructor(e,t,n){super(e,t,n),this.classes.push(td.primaryClass)}}Xo(td,"primaryClass","operation-tags");const nd=td,rd=Ys(Zc,{init(){this.element=new nd},methods:{ArrayElement(e){return this.element=this.element.concat(e.clone()),ei}}}),od=Zc,sd=Zc,id=Zc;class ad extends Pt.ON{constructor(e,t,n){super(e,t,n),this.classes.push(ad.primaryClass),this.classes.push("parameters")}}Xo(ad,"primaryClass","operation-parameters");const ld=ad,cd=Ys(Bc,Zc,{init(){this.element=new Pt.ON,this.element.classes.push("parameters")},methods:{ArrayElement(e){return e.forEach((e=>{const t=Uc(e)?["document","objects","Reference"]:["document","objects","Parameter"],n=this.toRefractedElement(t,e);tp(n)&&n.setMetaProperty("referenced-element","parameter"),this.element.push(n)})),this.copyMetaAndAttributes(e,this.element),ei}}}),ud=Ys(cd,{init(){this.element=new ld}}),pd=Ys(Fu,{props:{alternator:[{predicate:Uc,specPath:["document","objects","Reference"]},{predicate:Dt,specPath:["document","objects","RequestBody"]}]},methods:{ObjectElement(e){const t=Fu.compose.methods.enter.call(this,e);return tp(this.element)&&this.element.setMetaProperty("referenced-element","requestBody"),t}}});class hd extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(hd.primaryClass)}}Xo(hd,"primaryClass","operation-callbacks");const fd=hd,dd=Ys(yu,Zc,{props:{specPath:e=>Uc(e)?["document","objects","Reference"]:["document","objects","Callback"]},init(){this.element=new fd},methods:{ObjectElement(e){const t=yu.compose.methods.ObjectElement.call(this,e);return this.element.filter(tp).forEach((e=>{e.setMetaProperty("referenced-element","callback")})),t}}}),md=Zc;class gd extends Pt.ON{constructor(e,t,n){super(e,t,n),this.classes.push(gd.primaryClass),this.classes.push("security")}}Xo(gd,"primaryClass","operation-security");const yd=gd,vd=Ys(Bc,Zc,{init(){this.element=new yd},methods:{ArrayElement(e){return e.forEach((e=>{const t=bs(e)?["document","objects","SecurityRequirement"]:["value"],n=this.toRefractedElement(t,e);this.element.push(n)})),this.copyMetaAndAttributes(e,this.element),ei}}});class bd extends Pt.ON{constructor(e,t,n){super(e,t,n),this.classes.push(bd.primaryClass),this.classes.push("servers")}}Xo(bd,"primaryClass","operation-servers");const wd=bd,Ed=Ys(Au,{init(){this.element=new wd}}),xd=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","PathItem"])},init(){this.element=new Na},methods:{ObjectElement(e){const t=Gc.compose.methods.ObjectElement.call(this,e);return this.element.filter(Zu).forEach(((e,t)=>{const n=t.clone();n.content=n.toValue().toUpperCase(),e.setMetaProperty("http-method",n)})),ms(this.element.$ref)&&this.element.classes.push("reference-element"),t}}}),Sd=Ys(Zc,{methods:{StringElement(e){return this.element=e.clone(),this.element.classes.push("reference-value"),ei}}}),_d=Zc,jd=Zc;class Od extends Pt.ON{constructor(e,t,n){super(e,t,n),this.classes.push(Od.primaryClass),this.classes.push("servers")}}Xo(Od,"primaryClass","path-item-servers");const kd=Od,Ad=Ys(Au,{init(){this.element=new kd}});class Cd extends Pt.ON{constructor(e,t,n){super(e,t,n),this.classes.push(Cd.primaryClass),this.classes.push("parameters")}}Xo(Cd,"primaryClass","path-item-parameters");const Pd=Cd,Nd=Ys(cd,{init(){this.element=new Pd}}),Id=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","SecurityScheme"]),canSupportSpecificationExtensions:!0},init(){this.element=new Sc}}),Td=Zc,Rd=Zc,Md=Zc,Dd=Zc,Fd=Zc,Ld=Zc,Bd=Zc,$d=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","OAuthFlows"]),canSupportSpecificationExtensions:!0},init(){this.element=new Ea}}),qd=Ys(Gc,Zc,{props:{specPath:Hn(["document","objects","OAuthFlow"]),canSupportSpecificationExtensions:!0},init(){this.element=new ba}}),Ud=Zc,zd=Zc,Vd=Zc;class Wd extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(Wd.primaryClass)}}Xo(Wd,"primaryClass","oauth-flow-scopes");const Jd=Wd,Kd=Ys(yu,Zc,{props:{specPath:Hn(["value"])},init(){this.element=new Jd}});class Hd extends Pt.ON{constructor(e,t,n){super(e,t,n),this.classes.push(Hd.primaryClass)}}Xo(Hd,"primaryClass","tags");const Gd=Hd,Zd=Ys(Bc,Zc,{init(){this.element=new Gd},methods:{ArrayElement(e){return e.forEach((e=>{const t=Jc(e)?["document","objects","Tag"]:["value"],n=this.toRefractedElement(t,e);this.element.push(n)})),this.copyMetaAndAttributes(e,this.element),ei}}});function Yd(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function Xd(e){for(var t=1;t{const{base:t}=e;return t.register("callback",Gi),t.register("components",Yi),t.register("contact",Qi),t.register("discriminator",ta),t.register("encoding",ra),t.register("example",sa),t.register("externalDocumentation",aa),t.register("header",ca),t.register("info",pa),t.register("license",fa),t.register("link",ma),t.register("mediaType",ya),t.register("oAuthFlow",ba),t.register("oAuthFlows",Ea),t.register("openapi",Sa),t.register("openApi3_0",ja),t.register("operation",ka),t.register("parameter",Ca),t.register("pathItem",Na),t.register("paths",Ta),t.register("reference",Ma),t.register("requestBody",Fa),t.register("response",Ba),t.register("responses",qa),t.register("schema",bc),t.register("securityRequirement",Ec),t.register("securityScheme",Sc),t.register("server",jc),t.register("serverVariable",kc),t.register("tag",Cc),t.register("xml",Nc),t}};function rm(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function om(e){for(var t=1;t{const e=zs(nm);return{predicates:om(om(om({},a),l),{},{isStringElement:ms}),namespace:e}};function im(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}const am=(e,{specPath:t=["visitors","document","objects","OpenApi","$visitor"],plugins:n=[]}={})=>{const r=(0,Pt.Qc)(e),o=Za(tm),s=is(t,[],o);return fi(r,s,{state:{specObj:o}}),di(s.element,n,{toolboxCreator:sm,visitorOptions:{keyMap:Dc,nodeTypeGetter:Mc}})},lm=e=>(t,n={})=>am(t,function(e){for(var t=1;tr=>r instanceof cm||e(r)&&t("callback",r)&&n("object",r))),_g=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof um||e(r)&&t("components",r)&&n("object",r))),jg=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof pm||e(r)&&t("contact",r)&&n("object",r))),Og=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof dm||e(r)&&t("example",r)&&n("object",r))),kg=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof mm||e(r)&&t("externalDocumentation",r)&&n("object",r))),Ag=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof gm||e(r)&&t("header",r)&&n("object",r))),Cg=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof ym||e(r)&&t("info",r)&&n("object",r))),Pg=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof bm||e(r)&&t("jsonSchemaDialect",r)&&n("string",r))),Ng=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof wm||e(r)&&t("license",r)&&n("object",r))),Ig=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Em||e(r)&&t("link",r)&&n("object",r))),Tg=e=>{if(!Ig(e))return!1;if(!ms(e.operationRef))return!1;const t=e.operationRef.toValue();return"string"==typeof t&&t.length>0&&!t.startsWith("#")},Rg=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof jm||e(r)&&t("openapi",r)&&n("string",r))),Mg=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n,hasClass:r})=>o=>o instanceof km||e(o)&&t("openApi3_1",o)&&n("object",o)&&r("api",o))),Dg=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Am||e(r)&&t("operation",r)&&n("object",r))),Fg=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Cm||e(r)&&t("parameter",r)&&n("object",r))),Lg=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Pm||e(r)&&t("pathItem",r)&&n("object",r))),Bg=e=>{if(!Lg(e))return!1;if(!ms(e.$ref))return!1;const t=e.$ref.toValue();return"string"==typeof t&&t.length>0&&!t.startsWith("#")},$g=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Nm||e(r)&&t("paths",r)&&n("object",r))),qg=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Tm||e(r)&&t("reference",r)&&n("object",r))),Ug=e=>{if(!qg(e))return!1;if(!ms(e.$ref))return!1;const t=e.$ref.toValue();return"string"==typeof t&&t.length>0&&!t.startsWith("#")},zg=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Rm||e(r)&&t("requestBody",r)&&n("object",r))),Vg=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Mm||e(r)&&t("response",r)&&n("object",r))),Wg=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Dm||e(r)&&t("responses",r)&&n("object",r))),Jg=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Lm||e(r)&&t("schema",r)&&n("object",r))),Kg=e=>vs(e)&&e.classes.includes("boolean-json-schema"),Hg=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Bm||e(r)&&t("securityRequirement",r)&&n("object",r))),Gg=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof qm||e(r)&&t("server",r)&&n("object",r))),Zg=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof Um||e(r)&&t("serverVariable",r)&&n("object",r))),Yg=fs((({hasBasicElementProps:e,isElementType:t,primitiveEq:n})=>r=>r instanceof xm||e(r)&&t("mediaType",r)&&n("object",r))),Xg=Ys({props:{parent:null},init({parent:e=this.parent}){this.parent=e,this.passingOptionsNames=[...this.passingOptionsNames,"parent"]}}),Qg=Ys(Gc,Xg,Zc,{props:{specPath:Hn(["document","objects","Schema"]),canSupportSpecificationExtensions:!0},init(){const e=()=>{let e;return e=null!==this.openApiSemanticElement&&Pg(this.openApiSemanticElement.jsonSchemaDialect)?this.openApiSemanticElement.jsonSchemaDialect.toValue():null!==this.openApiGenericElement&&ms(this.openApiGenericElement.get("jsonSchemaDialect"))?this.openApiGenericElement.get("jsonSchemaDialect").toValue():bm.default.toValue(),e},t=t=>{if(Is(this.parent)&&!ms(t.get("$schema")))this.element.setMetaProperty("inherited$schema",e());else if(Jg(this.parent)&&!ms(t.get("$schema"))){var n,r;const e=kr(null===(n=this.parent.meta.get("inherited$schema"))||void 0===n?void 0:n.toValue(),null===(r=this.parent.$schema)||void 0===r?void 0:r.toValue());this.element.setMetaProperty("inherited$schema",e)}},n=e=>{var t;const n=null!==this.parent?this.parent.getMetaProperty("inherited$id",[]).clone():new Pt.ON,r=null===(t=e.get("$id"))||void 0===t?void 0:t.toValue();Nl(r)&&n.push(r),this.element.setMetaProperty("inherited$id",n)};this.ObjectElement=function(e){this.element=new Lm,t(e),n(e),this.parent=this.element;const r=Gc.compose.methods.ObjectElement.call(this,e);return ms(this.element.$ref)&&(this.element.classes.push("reference-element"),this.element.setMetaProperty("referenced-element","schema")),r},this.BooleanElement=function(e){return this.element=e.clone(),this.element.classes.push("boolean-json-schema"),ei}}}),ey=Zc,ty=Ys(Zc,{methods:{ObjectElement(e){return this.element=e.clone(),this.element.classes.push("json-schema-$vocabulary"),ei}}}),ny=Zc,ry=Zc,oy=Zc,sy=Zc,iy=Ys(Zc,{methods:{StringElement(e){return this.element=e.clone(),this.element.classes.push("reference-value"),ei}}}),ay=Ys(yu,Xg,Zc,{props:{specPath:Hn(["document","objects","Schema"])},init(){this.element=new Pt.Sb,this.element.classes.push("json-schema-$defs")}}),ly=Zc,cy=Ys(Bc,Xg,Zc,{init(){this.element=new Pt.ON,this.element.classes.push("json-schema-allOf")},methods:{ArrayElement(e){return e.forEach((e=>{if(bs(e)){const t=this.toRefractedElement(["document","objects","Schema"],e);this.element.push(t)}else{const t=e.clone();this.element.push(t)}})),this.copyMetaAndAttributes(e,this.element),ei}}}),uy=Ys(Bc,Xg,Zc,{init(){this.element=new Pt.ON,this.element.classes.push("json-schema-anyOf")},methods:{ArrayElement(e){return e.forEach((e=>{if(bs(e)){const t=this.toRefractedElement(["document","objects","Schema"],e);this.element.push(t)}else{const t=e.clone();this.element.push(t)}})),this.copyMetaAndAttributes(e,this.element),ei}}}),py=Ys(Bc,Xg,Zc,{init(){this.element=new Pt.ON,this.element.classes.push("json-schema-oneOf")},methods:{ArrayElement(e){return e.forEach((e=>{if(bs(e)){const t=this.toRefractedElement(["document","objects","Schema"],e);this.element.push(t)}else{const t=e.clone();this.element.push(t)}})),this.copyMetaAndAttributes(e,this.element),ei}}}),hy=Ys(yu,Xg,Zc,{props:{specPath:Hn(["document","objects","Schema"])},init(){this.element=new Pt.Sb,this.element.classes.push("json-schema-dependentSchemas")}}),fy=Ys(Bc,Xg,Zc,{init(){this.element=new Pt.ON,this.element.classes.push("json-schema-prefixItems")},methods:{ArrayElement(e){return e.forEach((e=>{if(bs(e)){const t=this.toRefractedElement(["document","objects","Schema"],e);this.element.push(t)}else{const t=e.clone();this.element.push(t)}})),this.copyMetaAndAttributes(e,this.element),ei}}}),dy=Ys(yu,Xg,Zc,{props:{specPath:Hn(["document","objects","Schema"])},init(){this.element=new Pt.Sb,this.element.classes.push("json-schema-properties")}}),my=Ys(yu,Xg,Zc,{props:{specPath:Hn(["document","objects","Schema"])},init(){this.element=new Pt.Sb,this.element.classes.push("json-schema-patternProperties")}}),gy=Ys(Zc,{methods:{StringElement(e){return this.element=e.clone(),this.element.classes.push("json-schema-type"),ei},ArrayElement(e){return this.element=e.clone(),this.element.classes.push("json-schema-type"),ei}}}),yy=Ys(Zc,{methods:{ArrayElement(e){return this.element=e.clone(),this.element.classes.push("json-schema-enum"),ei}}}),vy=Zc,by=Zc,wy=Zc,Ey=Zc,xy=Zc,Sy=Zc,_y=Zc,jy=Zc,Oy=Zc,ky=Zc,Ay=Zc,Cy=Zc,Py=Zc,Ny=Zc,Iy=Zc,Ty=Zc,Ry=Ys(Zc,{methods:{ArrayElement(e){return this.element=e.clone(),this.element.classes.push("json-schema-required"),ei}}}),My=Ys(Zc,{methods:{ObjectElement(e){return this.element=e.clone(),this.element.classes.push("json-schema-dependentRequired"),ei}}}),Dy=Zc,Fy=Zc,Ly=Zc,By=Zc,$y=Zc,qy=Zc,Uy=Ys(Zc,{methods:{ArrayElement(e){return this.element=e.clone(),this.element.classes.push("json-schema-examples"),ei}}}),zy=Zc,Vy=Zc,Wy=Zc,Jy=Zc,{visitors:{document:{objects:{Discriminator:{$visitor:Ky}}}}}=tm,Hy=Ys(Ky,{props:{canSupportSpecificationExtensions:!0},init(){this.element=new hm}}),{visitors:{document:{objects:{XML:{$visitor:Gy}}}}}=tm,Zy=Ys(Gy,{init(){this.element=new Vm}}),Yy=Ys(yu,Zc,{props:{specPath:Hn(["document","objects","Schema"])},init(){this.element=new Lh}});class Xy extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(Xy.primaryClass)}}Xo(Xy,"primaryClass","components-path-items");const Qy=Xy,ev=Ys(yu,Zc,{props:{specPath:e=>Uc(e)?["document","objects","Reference"]:["document","objects","PathItem"]},init(){this.element=new Qy},methods:{ObjectElement(e){const t=yu.compose.methods.ObjectElement.call(this,e);return this.element.filter(qg).forEach((e=>{e.setMetaProperty("referenced-element","pathItem")})),t}}}),{visitors:{document:{objects:{Example:{$visitor:tv}}}}}=tm,nv=Ys(tv,{init(){this.element=new dm}}),{visitors:{document:{objects:{ExternalDocumentation:{$visitor:rv}}}}}=tm,ov=Ys(rv,{init(){this.element=new mm}}),{visitors:{document:{objects:{Encoding:{$visitor:sv}}}}}=tm,iv=Ys(sv,{init(){this.element=new fm}}),{visitors:{document:{objects:{Paths:{$visitor:av}}}}}=tm,lv=Ys(av,{init(){this.element=new Nm}}),{visitors:{document:{objects:{RequestBody:{$visitor:cv}}}}}=tm,uv=Ys(cv,{init(){this.element=new Rm}}),{visitors:{document:{objects:{Callback:{$visitor:pv}}}}}=tm,hv=Ys(pv,{props:{specPath:e=>Uc(e)?["document","objects","Reference"]:["document","objects","PathItem"]},init(){this.element=new cm},methods:{ObjectElement(e){const t=pv.compose.methods.ObjectElement.call(this,e);return this.element.filter(qg).forEach((e=>{e.setMetaProperty("referenced-element","pathItem")})),t}}}),{visitors:{document:{objects:{Response:{$visitor:fv}}}}}=tm,dv=Ys(fv,{init(){this.element=new Mm}}),{visitors:{document:{objects:{Responses:{$visitor:mv}}}}}=tm,gv=Ys(mv,{init(){this.element=new Dm}}),{visitors:{document:{objects:{Operation:{$visitor:yv}}}}}=tm,vv=Ys(yv,{init(){this.element=new Am}}),{visitors:{document:{objects:{PathItem:{$visitor:bv}}}}}=tm,wv=Ys(bv,{init(){this.element=new Pm}}),{visitors:{document:{objects:{SecurityScheme:{$visitor:Ev}}}}}=tm,xv=Ys(Ev,{init(){this.element=new $m}}),{visitors:{document:{objects:{OAuthFlows:{$visitor:Sv}}}}}=tm,_v=Ys(Sv,{init(){this.element=new _m}}),{visitors:{document:{objects:{OAuthFlow:{$visitor:jv}}}}}=tm,Ov=Ys(jv,{init(){this.element=new Sm}});class kv extends Pt.Sb{constructor(e,t,n){super(e,t,n),this.classes.push(kv.primaryClass)}}Xo(kv,"primaryClass","webhooks");const Av=kv,Cv=Ys(yu,Zc,{props:{specPath:e=>Uc(e)?["document","objects","Reference"]:["document","objects","PathItem"]},init(){this.element=new Av},methods:{ObjectElement(e){const t=yu.compose.methods.ObjectElement.call(this,e);return this.element.filter(qg).forEach((e=>{e.setMetaProperty("referenced-element","pathItem")})),this.element.filter(Lg).forEach(((e,t)=>{e.setMetaProperty("webhook-name",t.toValue())})),t}}}),Pv={visitors:{value:tm.visitors.value,document:{objects:{OpenApi:{$visitor:Wm,fixedFields:{openapi:tm.visitors.document.objects.OpenApi.fixedFields.openapi,info:{$ref:"#/visitors/document/objects/Info"},jsonSchemaDialect:ng,servers:tm.visitors.document.objects.OpenApi.fixedFields.servers,paths:{$ref:"#/visitors/document/objects/Paths"},webhooks:Cv,components:{$ref:"#/visitors/document/objects/Components"},security:tm.visitors.document.objects.OpenApi.fixedFields.security,tags:tm.visitors.document.objects.OpenApi.fixedFields.tags,externalDocs:{$ref:"#/visitors/document/objects/ExternalDocumentation"}}},Info:{$visitor:Km,fixedFields:{title:tm.visitors.document.objects.Info.fixedFields.title,description:tm.visitors.document.objects.Info.fixedFields.description,summary:Hm,termsOfService:tm.visitors.document.objects.Info.fixedFields.termsOfService,contact:{$ref:"#/visitors/document/objects/Contact"},license:{$ref:"#/visitors/document/objects/License"},version:tm.visitors.document.objects.Info.fixedFields.version}},Contact:{$visitor:Zm,fixedFields:{name:tm.visitors.document.objects.Contact.fixedFields.name,url:tm.visitors.document.objects.Contact.fixedFields.url,email:tm.visitors.document.objects.Contact.fixedFields.email}},License:{$visitor:Xm,fixedFields:{name:tm.visitors.document.objects.License.fixedFields.name,identifier:Qm,url:tm.visitors.document.objects.License.fixedFields.url}},Server:{$visitor:og,fixedFields:{url:tm.visitors.document.objects.Server.fixedFields.url,description:tm.visitors.document.objects.Server.fixedFields.description,variables:tm.visitors.document.objects.Server.fixedFields.variables}},ServerVariable:{$visitor:ig,fixedFields:{enum:tm.visitors.document.objects.ServerVariable.fixedFields.enum,default:tm.visitors.document.objects.ServerVariable.fixedFields.default,description:tm.visitors.document.objects.ServerVariable.fixedFields.description}},Components:{$visitor:hg,fixedFields:{schemas:Yy,responses:tm.visitors.document.objects.Components.fixedFields.responses,parameters:tm.visitors.document.objects.Components.fixedFields.parameters,examples:tm.visitors.document.objects.Components.fixedFields.examples,requestBodies:tm.visitors.document.objects.Components.fixedFields.requestBodies,headers:tm.visitors.document.objects.Components.fixedFields.headers,securitySchemes:tm.visitors.document.objects.Components.fixedFields.securitySchemes,links:tm.visitors.document.objects.Components.fixedFields.links,callbacks:tm.visitors.document.objects.Components.fixedFields.callbacks,pathItems:ev}},Paths:{$visitor:lv},PathItem:{$visitor:wv,fixedFields:{$ref:tm.visitors.document.objects.PathItem.fixedFields.$ref,summary:tm.visitors.document.objects.PathItem.fixedFields.summary,description:tm.visitors.document.objects.PathItem.fixedFields.description,get:{$ref:"#/visitors/document/objects/Operation"},put:{$ref:"#/visitors/document/objects/Operation"},post:{$ref:"#/visitors/document/objects/Operation"},delete:{$ref:"#/visitors/document/objects/Operation"},options:{$ref:"#/visitors/document/objects/Operation"},head:{$ref:"#/visitors/document/objects/Operation"},patch:{$ref:"#/visitors/document/objects/Operation"},trace:{$ref:"#/visitors/document/objects/Operation"},servers:tm.visitors.document.objects.PathItem.fixedFields.servers,parameters:tm.visitors.document.objects.PathItem.fixedFields.parameters}},Operation:{$visitor:vv,fixedFields:{tags:tm.visitors.document.objects.Operation.fixedFields.tags,summary:tm.visitors.document.objects.Operation.fixedFields.summary,description:tm.visitors.document.objects.Operation.fixedFields.description,externalDocs:{$ref:"#/visitors/document/objects/ExternalDocumentation"},operationId:tm.visitors.document.objects.Operation.fixedFields.operationId,parameters:tm.visitors.document.objects.Operation.fixedFields.parameters,requestBody:tm.visitors.document.objects.Operation.fixedFields.requestBody,responses:{$ref:"#/visitors/document/objects/Responses"},callbacks:tm.visitors.document.objects.Operation.fixedFields.callbacks,deprecated:tm.visitors.document.objects.Operation.fixedFields.deprecated,security:tm.visitors.document.objects.Operation.fixedFields.security,servers:tm.visitors.document.objects.Operation.fixedFields.servers}},ExternalDocumentation:{$visitor:ov,fixedFields:{description:tm.visitors.document.objects.ExternalDocumentation.fixedFields.description,url:tm.visitors.document.objects.ExternalDocumentation.fixedFields.url}},Parameter:{$visitor:wg,fixedFields:{name:tm.visitors.document.objects.Parameter.fixedFields.name,in:tm.visitors.document.objects.Parameter.fixedFields.in,description:tm.visitors.document.objects.Parameter.fixedFields.description,required:tm.visitors.document.objects.Parameter.fixedFields.required,deprecated:tm.visitors.document.objects.Parameter.fixedFields.deprecated,allowEmptyValue:tm.visitors.document.objects.Parameter.fixedFields.allowEmptyValue,style:tm.visitors.document.objects.Parameter.fixedFields.style,explode:tm.visitors.document.objects.Parameter.fixedFields.explode,allowReserved:tm.visitors.document.objects.Parameter.fixedFields.allowReserved,schema:{$ref:"#/visitors/document/objects/Schema"},example:tm.visitors.document.objects.Parameter.fixedFields.example,examples:tm.visitors.document.objects.Parameter.fixedFields.examples,content:tm.visitors.document.objects.Parameter.fixedFields.content}},RequestBody:{$visitor:uv,fixedFields:{description:tm.visitors.document.objects.RequestBody.fixedFields.description,content:tm.visitors.document.objects.RequestBody.fixedFields.content,required:tm.visitors.document.objects.RequestBody.fixedFields.required}},MediaType:{$visitor:lg,fixedFields:{schema:{$ref:"#/visitors/document/objects/Schema"},example:tm.visitors.document.objects.MediaType.fixedFields.example,examples:tm.visitors.document.objects.MediaType.fixedFields.examples,encoding:tm.visitors.document.objects.MediaType.fixedFields.encoding}},Encoding:{$visitor:iv,fixedFields:{contentType:tm.visitors.document.objects.Encoding.fixedFields.contentType,headers:tm.visitors.document.objects.Encoding.fixedFields.headers,style:tm.visitors.document.objects.Encoding.fixedFields.style,explode:tm.visitors.document.objects.Encoding.fixedFields.explode,allowReserved:tm.visitors.document.objects.Encoding.fixedFields.allowReserved}},Responses:{$visitor:gv,fixedFields:{default:tm.visitors.document.objects.Responses.fixedFields.default}},Response:{$visitor:dv,fixedFields:{description:tm.visitors.document.objects.Response.fixedFields.description,headers:tm.visitors.document.objects.Response.fixedFields.headers,content:tm.visitors.document.objects.Response.fixedFields.content,links:tm.visitors.document.objects.Response.fixedFields.links}},Callback:{$visitor:hv},Example:{$visitor:nv,fixedFields:{summary:tm.visitors.document.objects.Example.fixedFields.summary,description:tm.visitors.document.objects.Example.fixedFields.description,value:tm.visitors.document.objects.Example.fixedFields.value,externalValue:tm.visitors.document.objects.Example.fixedFields.externalValue}},Link:{$visitor:tg,fixedFields:{operationRef:tm.visitors.document.objects.Link.fixedFields.operationRef,operationId:tm.visitors.document.objects.Link.fixedFields.operationId,parameters:tm.visitors.document.objects.Link.fixedFields.parameters,requestBody:tm.visitors.document.objects.Link.fixedFields.requestBody,description:tm.visitors.document.objects.Link.fixedFields.description,server:{$ref:"#/visitors/document/objects/Server"}}},Header:{$visitor:xg,fixedFields:{description:tm.visitors.document.objects.Header.fixedFields.description,required:tm.visitors.document.objects.Header.fixedFields.required,deprecated:tm.visitors.document.objects.Header.fixedFields.deprecated,allowEmptyValue:tm.visitors.document.objects.Header.fixedFields.allowEmptyValue,style:tm.visitors.document.objects.Header.fixedFields.style,explode:tm.visitors.document.objects.Header.fixedFields.explode,allowReserved:tm.visitors.document.objects.Header.fixedFields.allowReserved,schema:{$ref:"#/visitors/document/objects/Schema"},example:tm.visitors.document.objects.Header.fixedFields.example,examples:tm.visitors.document.objects.Header.fixedFields.examples,content:tm.visitors.document.objects.Header.fixedFields.content}},Tag:{$visitor:dg,fixedFields:{name:tm.visitors.document.objects.Tag.fixedFields.name,description:tm.visitors.document.objects.Tag.fixedFields.description,externalDocs:{$ref:"#/visitors/document/objects/ExternalDocumentation"}}},Reference:{$visitor:gg,fixedFields:{$ref:tm.visitors.document.objects.Reference.fixedFields.$ref,summary:yg,description:vg}},Schema:{$visitor:Qg,fixedFields:{$schema:ey,$vocabulary:ty,$id:ny,$anchor:ry,$dynamicAnchor:oy,$dynamicRef:sy,$ref:iy,$defs:ay,$comment:ly,allOf:cy,anyOf:uy,oneOf:py,not:{$ref:"#/visitors/document/objects/Schema"},if:{$ref:"#/visitors/document/objects/Schema"},then:{$ref:"#/visitors/document/objects/Schema"},else:{$ref:"#/visitors/document/objects/Schema"},dependentSchemas:hy,prefixItems:fy,items:{$ref:"#/visitors/document/objects/Schema"},contains:{$ref:"#/visitors/document/objects/Schema"},properties:dy,patternProperties:my,additionalProperties:{$ref:"#/visitors/document/objects/Schema"},propertyNames:{$ref:"#/visitors/document/objects/Schema"},unevaluatedItems:{$ref:"#/visitors/document/objects/Schema"},unevaluatedProperties:{$ref:"#/visitors/document/objects/Schema"},type:gy,enum:yy,const:vy,multipleOf:by,maximum:wy,exclusiveMaximum:Ey,minimum:xy,exclusiveMinimum:Sy,maxLength:_y,minLength:jy,pattern:Oy,maxItems:ky,minItems:Ay,uniqueItems:Cy,maxContains:Py,minContains:Ny,maxProperties:Iy,minProperties:Ty,required:Ry,dependentRequired:My,title:Dy,description:Fy,default:Ly,deprecated:By,readOnly:$y,writeOnly:qy,examples:Uy,format:zy,contentEncoding:Vy,contentMediaType:Wy,contentSchema:{$ref:"#/visitors/document/objects/Schema"},discriminator:{$ref:"#/visitors/document/objects/Discriminator"},xml:{$ref:"#/visitors/document/objects/XML"},externalDocs:{$ref:"#/visitors/document/objects/ExternalDocumentation"},example:Jy}},Discriminator:{$visitor:Hy,fixedFields:{propertyName:tm.visitors.document.objects.Discriminator.fixedFields.propertyName,mapping:tm.visitors.document.objects.Discriminator.fixedFields.mapping}},XML:{$visitor:Zy,fixedFields:{name:tm.visitors.document.objects.XML.fixedFields.name,namespace:tm.visitors.document.objects.XML.fixedFields.namespace,prefix:tm.visitors.document.objects.XML.fixedFields.prefix,attribute:tm.visitors.document.objects.XML.fixedFields.attribute,wrapped:tm.visitors.document.objects.XML.fixedFields.wrapped}},SecurityScheme:{$visitor:xv,fixedFields:{type:tm.visitors.document.objects.SecurityScheme.fixedFields.type,description:tm.visitors.document.objects.SecurityScheme.fixedFields.description,name:tm.visitors.document.objects.SecurityScheme.fixedFields.name,in:tm.visitors.document.objects.SecurityScheme.fixedFields.in,scheme:tm.visitors.document.objects.SecurityScheme.fixedFields.scheme,bearerFormat:tm.visitors.document.objects.SecurityScheme.fixedFields.bearerFormat,flows:{$ref:"#/visitors/document/objects/OAuthFlows"},openIdConnectUrl:tm.visitors.document.objects.SecurityScheme.fixedFields.openIdConnectUrl}},OAuthFlows:{$visitor:_v,fixedFields:{implicit:{$ref:"#/visitors/document/objects/OAuthFlow"},password:{$ref:"#/visitors/document/objects/OAuthFlow"},clientCredentials:{$ref:"#/visitors/document/objects/OAuthFlow"},authorizationCode:{$ref:"#/visitors/document/objects/OAuthFlow"}}},OAuthFlow:{$visitor:Ov,fixedFields:{authorizationUrl:tm.visitors.document.objects.OAuthFlow.fixedFields.authorizationUrl,tokenUrl:tm.visitors.document.objects.OAuthFlow.fixedFields.tokenUrl,refreshUrl:tm.visitors.document.objects.OAuthFlow.fixedFields.refreshUrl,scopes:tm.visitors.document.objects.OAuthFlow.fixedFields.scopes}},SecurityRequirement:{$visitor:ug}},extension:{$visitor:tm.visitors.document.extension.$visitor}}}};function Nv(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}const Iv=e=>{if(ds(e))return`${e.element.charAt(0).toUpperCase()+e.element.slice(1)}Element`},Tv=function(e){for(var t=1;t{const{base:t}=e;return t.register("callback",cm),t.register("components",um),t.register("contact",pm),t.register("discriminator",hm),t.register("encoding",fm),t.register("example",dm),t.register("externalDocumentation",mm),t.register("header",gm),t.register("info",ym),t.register("jsonSchemaDialect",bm),t.register("license",wm),t.register("link",Em),t.register("mediaType",xm),t.register("oAuthFlow",Sm),t.register("oAuthFlows",_m),t.register("openapi",jm),t.register("openApi3_1",km),t.register("operation",Am),t.register("parameter",Cm),t.register("pathItem",Pm),t.register("paths",Nm),t.register("reference",Tm),t.register("requestBody",Rm),t.register("response",Mm),t.register("responses",Dm),t.register("schema",Lm),t.register("securityRequirement",Bm),t.register("securityScheme",$m),t.register("server",qm),t.register("serverVariable",Um),t.register("tag",zm),t.register("xml",Vm),t}};function Mv(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function Dv(e){for(var t=1;t{const e=zs(Rv);return{predicates:Dv(Dv({},c),{},{isStringElement:ms,isArrayElement:ws,isObjectElement:bs,includesClasses:Ns}),namespace:e}};function Lv(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}const Bv=(e,{specPath:t=["visitors","document","objects","OpenApi","$visitor"],plugins:n=[]}={})=>{const r=(0,Pt.Qc)(e),o=Za(Pv),s=is(t,[],o);return fi(r,s,{state:{specObj:o}}),di(s.element,n,{toolboxCreator:Fv,visitorOptions:{keyMap:Tv,nodeTypeGetter:Iv}})},$v=e=>(t,n={})=>Bv(t,function(e){for(var t=1;te.includes(t)))}findBy(e="3.1.0",t="generic"){const n="generic"===t?`vnd.oai.openapi;version=${e}`:`vnd.oai.openapi+${t};version=${e}`;return this.find((e=>e.includes(n)))||this.unknownMediaType}latest(e="generic"){return ao(this.filterByFormat(e))}}const zv=new Uv("application/vnd.oai.openapi;version=3.1.0","application/vnd.oai.openapi+json;version=3.1.0","application/vnd.oai.openapi+yaml;version=3.1.0");var Vv=n(34155),Wv=Or((function(e,t){return gr(Io(""),Fr(as(e)),io(""))(t)}));const Jv=Wv;const Kv=pr(qo);const Hv=Zt(1,gr(cn,Xr("RegExp")));const Gv=Bo(Xs,Co(/[.*+?^${}()|[\]\\-]/g,"\\$&"));var Zv=function(e,t){if("string"!=typeof e&&!(e instanceof String))throw TypeError("`".concat(t,"` must be a string"))};var Yv=Zt(3,(function(e,t,n){!function(e,t,n){if(null==n||null==e||null==t)throw TypeError("Input values must not be `null` or `undefined`")}(e,t,n),Zv(n,"str"),Zv(t,"replaceValue"),function(e){if(!("string"==typeof e||e instanceof String||e instanceof RegExp))throw TypeError("`searchValue` must be a string or an regexp")}(e);var r=new RegExp(Hv(e)?e:Gv(e),"g");return Co(r,t,n)})),Xv=oo(2,"replaceAll");const Qv=ts(String.prototype.replaceAll)?Xv:Yv,eb=()=>wo(Ro(/^win/),["platform"],Vv),tb=e=>{try{const t=new URL(e);return Jv(":",t.protocol)}catch{return}},nb=(gr(tb,Kv),e=>{if(Vv.browser)return!1;const t=tb(e);return qo(t)||"file"===t||/^[a-zA-Z]$/.test(t)}),rb=e=>{const t=tb(e);return"http"===t||"https"===t},ob=(e,t)=>{const n=[/%23/g,"#",/%24/g,"$",/%26/g,"&",/%2C/g,",",/%40/g,"@"],r=So(!1,"keepFileProtocol",t),o=So(eb,"isWindows",t);let s=decodeURI(e);for(let e=0;e{const t=e.indexOf("#");return-1!==t?e.substr(t):"#"},ib=e=>{const t=e.indexOf("#");let n=e;return t>=0&&(n=e.substr(0,t)),n},ab=()=>{if(Vv.browser)return ib(globalThis.location.href);const e=Vv.cwd(),t=ao(e);return["/","\\"].includes(t)?e:e+(eb()?"\\":"/")},lb=(e,t)=>{const n=new URL(t,new URL(e,"resolve://"));if("resolve:"===n.protocol){const{pathname:e,search:t,hash:r}=n;return e+t+r}return n.toString()},cb=e=>nb(e)?(e=>{const t=[/\?/g,"%3F",/#/g,"%23"];let n=e;eb()&&(n=n.replace(/\\/g,"/")),n=encodeURI(n);for(let e=0;enb(e)?ob(e):decodeURI(e),pb=Ys({props:{uri:"",value:null,depth:0,refSet:null,errors:[]},init({depth:e=this.depth,refSet:t=this.refSet,uri:n=this.uri,value:r=this.value}={}){this.uri=n,this.value=r,this.depth=e,this.refSet=t,this.errors=[]}}),hb=pb,fb=Ys({props:{rootRef:null,refs:[],circular:!1},init({refs:e=[]}={}){this.refs=[],e.forEach((e=>this.add(e)))},methods:{get size(){return this.refs.length},add(e){return this.has(e)||(this.refs.push(e),this.rootRef=null===this.rootRef?e:this.rootRef,e.refSet=this),this},merge(e){for(const t of e.values())this.add(t);return this},has(e){const t=Xs(e)?e:e.uri;return Kv(this.find(xo(t,"uri")))},find(e){return this.refs.find(e)},*values(){yield*this.refs},clean(){this.refs.forEach((e=>{e.refSet=null})),this.refs=[]}}}),db=fb,mb={parse:{mediaType:"text/plain",parsers:[],parserOpts:{}},resolve:{baseURI:"",resolvers:[],resolverOpts:{},strategies:[],external:!0,maxDepth:1/0},dereference:{strategies:[],refSet:null,maxDepth:1/0}},gb=lo(uo(["resolve","baseURI"]),or(["resolve","baseURI"])),yb=e=>Ri(e)?ab():e,vb=Ys({props:{uri:null,mediaType:"text/plain",data:null,parseResult:null},init({uri:e=this.uri,mediaType:t=this.mediaType,data:n=this.data,parseResult:r=this.parseResult}={}){this.uri=e,this.mediaType=t,this.data=n,this.parseResult=r},methods:{get extension(){return Xs(this.uri)?(e=>{const t=e.lastIndexOf(".");return t>=0?e.substr(t).toLowerCase():""})(this.uri):""},toString(){if("string"==typeof this.data)return this.data;if(this.data instanceof ArrayBuffer||["ArrayBuffer"].includes(cn(this.data))||ArrayBuffer.isView(this.data)){return new TextDecoder("utf-8").decode(this.data)}return String(this.data)}}});class bb extends Error{constructor(e,t){if(super(e),this.name=this.constructor.name,this.message=e,"function"==typeof Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=new Error(e).stack,$s(t)&&Gr("cause",t)&&!Gr("cause",this)){const{cause:e}=t;this.cause=e,Gr("stack",e)&&(this.stack=`${this.stack}\nCAUSE: ${null==e?void 0:e.stack}`)}}}const wb=bb;const Eb=class extends wb{constructor(e,t){super(e,{cause:t.cause}),Xo(this,"plugin",void 0),this.plugin=t.plugin}},xb=async(e,t,n)=>{const r=await Promise.all(n.map(is([e],[t])));return n.filter(((e,t)=>r[t]))},Sb=async(e,t,n)=>{let r;for(const o of n)try{const n=await o[e].call(o,...t);return{plugin:o,result:n}}catch(e){r=new Eb("Error while running plugin",{cause:e,plugin:o})}return Promise.reject(r)};const _b=class extends wb{};const jb=class extends _b{};const Ob=class extends wb{},kb=async(e,t)=>{let n=e,r=!1;if(!Os(e)){const t=new e.constructor(e.content,e.meta.clone(),e.attributes);t.classes.push("result"),n=new zo([t]),r=!0}const o=vb({uri:t.resolve.baseURI,parseResult:n,mediaType:t.parse.mediaType}),s=await xb("canDereference",o,t.dereference.strategies);if(so(s))throw new jb(o.uri);try{const{result:e}=await Sb("dereference",[o,t],s);return r?e.get(0):e}catch(e){throw new Ob(`Error while dereferencing file "${o.uri}"`,{cause:e})}},Ab=async(e,t={})=>{const n=((e,t)=>{const n=mo(e,t);return vo(gb,yb,n)})(mb,t);return kb(e,n)};const Cb=class extends wb{constructor(e="Not Implemented",t){super(e,t)}},Pb=Ys({props:{name:"",allowEmpty:!0,sourceMap:!1,fileExtensions:[],mediaTypes:[]},init({allowEmpty:e=this.allowEmpty,sourceMap:t=this.sourceMap,fileExtensions:n=this.fileExtensions,mediaTypes:r=this.mediaTypes}={}){this.allowEmpty=e,this.sourceMap=t,this.fileExtensions=n,this.mediaTypes=r},methods:{async canParse(){throw new Cb},async parse(){throw new Cb}}}),Nb=Pb,Ib=Ys(Nb,{props:{name:"binary"},methods:{async canParse(e){return 0===this.fileExtensions.length||this.fileExtensions.includes(e.extension)},async parse(e){try{const t=unescape(encodeURIComponent(e.toString())),n=btoa(t),r=new zo;if(0!==n.length){const e=new Pt.RP(n);e.classes.push("result"),r.push(e)}return r}catch(t){throw new _b(`Error parsing "${e.uri}"`,{cause:t})}}}}),Tb=Ys({props:{name:null},methods:{canResolve:()=>!1,async resolve(){throw new Cb}}});const Rb=Zt(1,$n(Promise.all,Promise));const Mb=class extends wb{};const Db=class extends Mb{};const Fb=class extends Ob{};const Lb=class extends Mb{};function Bb(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function $b(e){for(var t=1;t{const n=vb({uri:cb(ib(e)),mediaType:t.parse.mediaType}),r=await(async(e,t)=>{const n=t.resolve.resolvers.map((e=>{const n=Object.create(e);return Object.assign(n,t.resolve.resolverOpts)})),r=await xb("canRead",e,n);if(so(r))throw new Lb(e.uri);try{const{result:t}=await Sb("read",[e],r);return t}catch(t){throw new Mb(`Error while reading file "${e.uri}"`,{cause:t})}})(n,t);return(async(e,t)=>{const n=t.parse.parsers.map((e=>{const n=Object.create(e);return Object.assign(n,t.parse.parserOpts)})),r=await xb("canParse",e,n);if(so(r))throw new Lb(e.uri);try{const{plugin:t,result:n}=await Sb("parse",[e],r);return!t.allowEmpty&&n.isEmpty?Promise.reject(new _b(`Error while parsing file "${e.uri}". File is empty.`)):n}catch(t){throw new _b(`Error while parsing file "${e.uri}"`,{cause:t})}})(vb($b($b({},n),{},{data:r})),t)},Ub=(e,t)=>{const n=hi({predicate:e});return fi(t,n),new Pt.O4(n.result)};class zb extends Error{constructor(e){super(e),this.name=this.constructor.name,this.message=e,"function"==typeof Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=new Error(e).stack}}const Vb=(e,t)=>{const n=hi({predicate:e,returnOnTrue:ei});return fi(t,n),bo(void 0,[0],n.result)};const Wb=class extends wb{};class Jb extends Wb{constructor(e){super(`Invalid JSON Schema $anchor "${e}".`)}}class Kb extends Error{constructor(e){super(e),this.name=this.constructor.name,this.message=e,"function"==typeof Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=new Error(e).stack}}const Hb=e=>/^[A-Za-z_][A-Za-z_0-9.-]*$/.test(e),Gb=e=>{const t=sb(e);return qi("#",t)},Zb=(e,t)=>{const n=(e=>{if(!Hb(e))throw new Jb(e);return e})(e),r=Vb((e=>{var t;return Jg(e)&&(null===(t=e.$anchor)||void 0===t?void 0:t.toValue())===n}),t);if(qo(r))throw new Kb(`Evaluation failed on token: "${n}"`);return r},Yb=(e,t)=>{if(void 0===t.$ref)return;const n=sb(t.$ref.toValue()),r=t.meta.get("inherited$id").toValue();return`${Jn(((e,t)=>lb(e,cb(ib(t)))),e,[...r,t.$ref.toValue()])}${"#"===n?"":n}`},Xb=e=>{if(Xb.cache.has(e))return Xb.cache.get(e);const t=Lm.refract(e);return Xb.cache.set(e,t),t};Xb.cache=new WeakMap;const Qb=e=>As(e)?Xb(e):e,ew=(e,t)=>{const{cache:n}=ew,r=ib(e),o=e=>Jg(e)&&void 0!==e.$id;if(!n.has(t)){const e=Ub(o,t);n.set(t,Array.from(e))}const s=n.get(t).find((e=>((e,t)=>{if(void 0===t.$id)return;const n=t.meta.get("inherited$id").toValue();return Jn(((e,t)=>lb(e,cb(ib(t)))),e,[...n,t.$id.toValue()])})(r,e)===r));if(qo(s))throw new zb(`Evaluation failed on URI: "${e}"`);let i,a;return Hb(Gb(e))?(i=Zb,a=Gb(e)):(i=Ji,a=Ki(e)),i(a,s)};function tw(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function nw(e){for(var t=1;t=this.options.resolve.maxDepth)throw new Db(`Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file "${this.reference.uri}"`);const t=this.toBaseURI(e),{refSet:n}=this.reference;if(n.has(t))return n.find(xo(t,"uri"));const r=await qb(ub(t),nw(nw({},this.options),{},{parse:nw(nw({},this.options.parse),{},{mediaType:"text/plain"})})),o=hb({uri:t,value:r,depth:this.reference.depth+1});return n.add(o),o},ReferenceElement(e){var t;if(!this.options.resolve.external&&Ug(e))return!1;const n=null===(t=e.$ref)||void 0===t?void 0:t.toValue(),r=this.toBaseURI(n);Hr(r,this.crawlingMap)||(this.crawlingMap[r]=this.toReference(n)),this.crawledElements.push(e)},PathItemElement(e){var t;if(!ms(e.$ref))return;if(!this.options.resolve.external&&Bg(e))return;const n=null===(t=e.$ref)||void 0===t?void 0:t.toValue(),r=this.toBaseURI(n);Hr(r,this.crawlingMap)||(this.crawlingMap[r]=this.toReference(n)),this.crawledElements.push(e)},LinkElement(e){if((ms(e.operationRef)||ms(e.operationId))&&(this.options.resolve.external||!Tg(e))){if(ms(e.operationRef)&&ms(e.operationId))throw new Error("LinkElement operationRef and operationId are mutually exclusive.");if(Tg(e)){var t;const n=null===(t=e.operationRef)||void 0===t?void 0:t.toValue(),r=this.toBaseURI(n);Hr(r,this.crawlingMap)||(this.crawlingMap[r]=this.toReference(n))}}},ExampleElement(e){var t;if(!ms(e.externalValue))return;if(!this.options.resolve.external&&ms(e.externalValue))return;if(e.hasKey("value")&&ms(e.externalValue))throw new Error("ExampleElement value and externalValue fields are mutually exclusive.");const n=null===(t=e.externalValue)||void 0===t?void 0:t.toValue(),r=this.toBaseURI(n);Hr(r,this.crawlingMap)||(this.crawlingMap[r]=this.toReference(n))},SchemaElement(e){if(this.visited.has(e))return!1;if(!ms(e.$ref))return void this.visited.add(e);const t=this.reference.uri,n=Yb(t,e),r=ib(n),o=vb({uri:r}),s=go((e=>e.canRead(o)),this.options.resolve.resolvers),i=!s,a=!s&&this.reference.uri!==r;if(this.options.resolve.external||!a){if(!Hr(r,this.crawlingMap))try{this.crawlingMap[r]=s||i?this.reference:this.toReference(ub(n))}catch(e){if(!(i&&e instanceof zb))throw e;this.crawlingMap[r]=this.toReference(ub(n))}this.crawledElements.push(e)}else this.visited.add(e)},async crawlReferenceElement(e){var t;const n=await this.toReference(e.$ref.toValue());this.indirections.push(e);const r=Ki(null===(t=e.$ref)||void 0===t?void 0:t.toValue());let o=Ji(r,n.value.result);if(As(o)){const t=e.meta.get("referenced-element").toValue();if(Uc(o))o=Tm.refract(o),o.setMetaProperty("referenced-element",t);else{o=this.namespace.getElementClass(t).refract(o)}}if(this.indirections.includes(o))throw new Error("Recursive Reference Object detected");if(this.indirections.length>this.options.dereference.maxDepth)throw new Fb(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`);const s=ow({reference:n,namespace:this.namespace,indirections:[...this.indirections],options:this.options});await rw(o,s,{keyMap:Tv,nodeTypeGetter:Iv}),await s.crawl(),this.indirections.pop()},async crawlPathItemElement(e){var t;const n=await this.toReference(e.$ref.toValue());this.indirections.push(e);const r=Ki(null===(t=e.$ref)||void 0===t?void 0:t.toValue());let o=Ji(r,n.value.result);if(As(o)&&(o=Pm.refract(o)),this.indirections.includes(o))throw new Error("Recursive Path Item Object reference detected");if(this.indirections.length>this.options.dereference.maxDepth)throw new Fb(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`);const s=ow({reference:n,namespace:this.namespace,indirections:[...this.indirections],options:this.options});await rw(o,s,{keyMap:Tv,nodeTypeGetter:Iv}),await s.crawl(),this.indirections.pop()},async crawlSchemaElement(e){const t=this.reference.uri,n=Yb(t,e),r=ib(n),o=vb({uri:r}),s=go((e=>e.canRead(o)),this.options.resolve.resolvers),i=!s;let a,l;this.indirections.push(e);try{if(s||i){a=this.reference;l=ew(n,Qb(a.value.result))}else{a=await this.toReference(ub(n));const e=Ki(n);l=Qb(Ji(e,a.value.result))}}catch(e){if(!(i&&e instanceof zb))throw e;if(Hb(Gb(n))){a=await this.toReference(ub(n));const e=Gb(n);l=Zb(e,Qb(a.value.result))}else{a=await this.toReference(ub(n));const e=Ki(n);l=Qb(Ji(e,a.value.result))}}if(this.visited.add(e),this.indirections.includes(l))throw new Error("Recursive Schema Object reference detected");if(this.indirections.length>this.options.dereference.maxDepth)throw new Fb(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`);const c=ow({reference:a,namespace:this.namespace,indirections:[...this.indirections],options:this.options,visited:this.visited});await rw(l,c,{keyMap:Tv,nodeTypeGetter:Iv}),await c.crawl(),this.indirections.pop()},async crawl(){await gr(nr,Rb)(this.crawlingMap),this.crawlingMap=null;for(const e of this.crawledElements)qg(e)?await this.crawlReferenceElement(e):Jg(e)?await this.crawlSchemaElement(e):Lg(e)&&await this.crawlPathItemElement(e)}}}),sw=ow,iw=fi[Symbol.for("nodejs.util.promisify.custom")],aw=Ys(Tb,{init(){this.name="openapi-3-1"},methods:{canResolve(e){var t;return"text/plain"!==e.mediaType?zv.includes(e.mediaType):Mg(null===(t=e.parseResult)||void 0===t?void 0:t.result)},async resolve(e,t){const n=zs(Rv),r=hb({uri:e.uri,value:e.parseResult}),o=sw({reference:r,namespace:n,options:t}),s=db();return s.add(r),await iw(s.rootRef.value,o,{keyMap:Tv,nodeTypeGetter:Iv}),await o.crawl(),s}}}),lw=aw,cw=e=>e.replace(/\s/g,""),uw=e=>e.replace(/\W/gi,"_"),pw=(e,t,n)=>{const r=cw(e);return r.length>0?uw(r):((e,t)=>`${uw(cw(t.toLowerCase()))}${uw(cw(e))}`)(t,n)},hw=({operationIdNormalizer:e=pw}={})=>({predicates:t,namespace:n})=>{const r=[],o=[],s=[];return{visitor:{OpenApi3_1Element:{leave(){const e=Jr((e=>Ti(e.operationId)),o);Object.entries(e).forEach((([e,t])=>{t.length<=1||t.forEach(((t,r)=>{const o=`${e}${r+1}`;t.operationId=new n.elements.String(o)}))})),s.forEach((e=>{var t;if(void 0===e.operationId)return;const n=String(Ti(e.operationId)),r=o.find((e=>Ti(e.meta.get("originalOperationId"))===n));void 0!==r&&(e.operationId=null===(t=r.operationId)||void 0===t?void 0:t.clone(),e.meta.set("originalOperationId",n),e.set("__originalOperationId",n))})),o.length=0,s.length=0}},PathItemElement:{enter(e){const t=kr("path",Ti(e.meta.get("path")));r.push(t)},leave(){r.pop()}},OperationElement:{enter(t){if(void 0===t.operationId)return;const s=String(Ti(t.operationId)),i=ao(r),a=kr("method",Ti(t.meta.get("http-method"))),l=e(s,i,a);s!==l&&(t.operationId=new n.elements.String(l),t.set("__originalOperationId",s),t.meta.set("originalOperationId",s),o.push(t))}},LinkElement:{leave(e){t.isLinkElement(e)&&void 0!==e.operationId&&s.push(e)}}}}},fw=()=>({predicates:e})=>{const t=(t,n)=>!!e.isParameterElement(t)&&(!!e.isParameterElement(n)&&(!!e.isStringElement(t.name)&&(!!e.isStringElement(t.in)&&(!!e.isStringElement(n.name)&&(!!e.isStringElement(n.in)&&(Ti(t.name)===Ti(n.name)&&Ti(t.in)===Ti(n.in))))))),n=[];return{visitor:{PathItemElement:{enter(t,r,o,s,i){if(i.some(e.isComponentsElement))return;const{parameters:a}=t;e.isArrayElement(a)?n.push([...a.content]):n.push([])},leave(){n.pop()}},OperationElement:{leave(e){const r=ao(n);if(!Array.isArray(r)||0===r.length)return;const o=bo([],["parameters","content"],e),s=Lo(t,[...o,...r]);e.parameters=new ld(s)}}}}},dw=()=>({predicates:e})=>{let t;return{visitor:{OpenApi3_1Element:{enter(n){e.isArrayElement(n.security)&&(t=n.security)},leave(){t=void 0}},OperationElement:{leave(n,r,o,s,i){if(i.some(e.isComponentsElement))return;var a;void 0===n.security&&void 0!==t&&(n.security=new yd(null===(a=t)||void 0===a?void 0:a.content))}}}}},mw=()=>({predicates:e})=>{let t;const n=[];return{visitor:{OpenApi3_1Element:{enter(n){var r;e.isArrayElement(n.servers)&&(t=null===(r=n.servers)||void 0===r?void 0:r.content)},leave(){t=void 0}},PathItemElement:{enter(r,o,s,i,a){if(a.some(e.isComponentsElement))return;void 0===r.servers&&void 0!==t&&(r.servers=new kd(t));const{servers:l}=r;void 0!==l&&e.isArrayElement(l)?n.push([...l.content]):n.push(void 0)},leave(){n.pop()}},OperationElement:{enter(t){const r=ao(n);void 0!==r&&(e.isArrayElement(t.servers)||(t.servers=new wd(r)))}}}}},gw=()=>({predicates:e})=>({visitor:{ParameterElement:{leave(t,n,r,o,s){var i,a;if(!s.some(e.isComponentsElement)&&void 0!==t.schema&&e.isSchemaElement(t.schema)&&(void 0!==(null===(i=t.schema)||void 0===i?void 0:i.example)||void 0!==(null===(a=t.schema)||void 0===a?void 0:a.examples))){if(void 0!==t.examples&&e.isObjectElement(t.examples)){const e=t.examples.map((e=>{var t;return null===(t=e.value)||void 0===t?void 0:t.clone()}));return void 0!==t.schema.examples&&t.schema.set("examples",e),void(void 0!==t.schema.example&&t.schema.set("example",e))}void 0!==t.example&&(void 0!==t.schema.examples&&t.schema.set("examples",[t.example.clone()]),void 0!==t.schema.example&&t.schema.set("example",t.example.clone()))}}}}}),yw=()=>({predicates:e})=>({visitor:{HeaderElement:{leave(t,n,r,o,s){var i,a;if(!s.some(e.isComponentsElement)&&void 0!==t.schema&&e.isSchemaElement(t.schema)&&(void 0!==(null===(i=t.schema)||void 0===i?void 0:i.example)||void 0!==(null===(a=t.schema)||void 0===a?void 0:a.examples))){if(void 0!==t.examples&&e.isObjectElement(t.examples)){const e=t.examples.map((e=>{var t;return null===(t=e.value)||void 0===t?void 0:t.clone()}));return void 0!==t.schema.examples&&t.schema.set("examples",e),void(void 0!==t.schema.example&&t.schema.set("example",e))}void 0!==t.example&&(void 0!==t.schema.examples&&t.schema.set("examples",[t.example.clone()]),void 0!==t.schema.example&&t.schema.set("example",t.example.clone()))}}}}}),vw=e=>t=>{if(t?.$$normalized)return t;if(vw.cache.has(t))return t;const n=km.refract(t),r=e(n),o=Ti(r);return vw.cache.set(t,o),o};vw.cache=new WeakMap;const bw=e=>{if(!bs(e))return e;if(e.hasKey("$$normalized"))return e;const t=[hw({operationIdNormalizer:(e,t,n)=>(0,He.Z)({operationId:e},t,n,{v2OperationIdCompatibilityMode:!1})}),fw(),dw(),mw(),gw(),yw()],n=di(e,t,{toolboxCreator:Fv,visitorOptions:{keyMap:Tv,nodeTypeGetter:Iv}});return n.set("$$normalized",!0),n},ww=Ys({props:{name:null},methods:{canRead:()=>!1,async read(){throw new Cb}}}),Ew=Ys(ww,{props:{timeout:5e3,redirects:5,withCredentials:!1},init({timeout:e=this.timeout,redirects:t=this.redirects,withCredentials:n=this.withCredentials}={}){this.timeout=e,this.redirects=t,this.withCredentials=n},methods:{canRead:e=>rb(e.uri),async read(){throw new Cb},getHttpClient(){throw new Cb}}}).compose({props:{name:"http-swagger-client",swaggerHTTPClient:ct,swaggerHTTPClientConfig:{}},init(){let{swaggerHTTPClient:e=this.swaggerHTTPClient}=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};this.swaggerHTTPClient=e},methods:{getHttpClient(){return this.swaggerHTTPClient},async read(e){const t=this.getHttpClient(),n=new AbortController,{signal:r}=n,o=setTimeout((()=>{n.abort()}),this.timeout),s=this.getHttpClient().withCredentials||this.withCredentials?"include":"same-origin",i=0===this.redirects?"error":"follow",a=this.redirects>0?this.redirects:void 0;try{return(await t(f()({url:e.uri,signal:r,userFetch:async(e,t)=>{let n=await fetch(e,t);try{n.headers.delete("Content-Type")}catch{n=new Response(n.body,f()(f()({},n),{},{headers:new Headers(n.headers)})),n.headers.delete("Content-Type")}return n},credentials:s,redirects:i,follow:a},this.swaggerHTTPClientConfig))).text.arrayBuffer()}catch(t){throw new Mb(`Error downloading "${e.uri}"`,{cause:t})}finally{clearTimeout(o)}}}}),xw=Nb.compose({props:{name:"json-swagger-client",fileExtensions:[".json"],mediaTypes:["application/json"]},methods:{async canParse(e){const t=0===this.fileExtensions.length||this.fileExtensions.includes(e.extension),n=this.mediaTypes.includes(e.mediaType);if(!t)return!1;if(n)return!0;if(!n)try{return JSON.parse(e.toString()),!0}catch(e){return!1}return!1},async parse(e){if(this.sourceMap)throw new _b("json-swagger-client parser plugin doesn't support sourceMaps option");const t=new zo,n=e.toString();if(this.allowEmpty&&""===n.trim())return t;try{const e=Ii(JSON.parse(n));return e.classes.push("result"),t.push(e),t}catch(t){throw new _b(`Error parsing "${e.uri}"`,{cause:t})}}}}),Sw=Nb.compose({props:{name:"yaml-1-2-swagger-client",fileExtensions:[".yaml",".yml"],mediaTypes:["text/yaml","application/yaml"]},methods:{async canParse(e){const t=0===this.fileExtensions.length||this.fileExtensions.includes(e.extension),n=this.mediaTypes.includes(e.mediaType);if(!t)return!1;if(n)return!0;if(!n)try{return le.ZP.load(e.toString(),{schema:le.A8}),!0}catch(e){return!1}return!1},async parse(e){if(this.sourceMap)throw new _b("yaml-1-2-swagger-client parser plugin doesn't support sourceMaps option");const t=new zo,n=e.toString();try{const e=le.ZP.load(n,{schema:le.A8});if(this.allowEmpty&&void 0===e)return t;const r=Ii(e);return r.classes.push("result"),t.push(r),t}catch(t){throw new _b(`Error parsing "${e.uri}"`,{cause:t})}}}}),_w=Nb.compose({props:{name:"openapi-json-3-1-swagger-client",fileExtensions:[".json"],mediaTypes:new Uv(...zv.filterByFormat("generic"),...zv.filterByFormat("json")),detectionRegExp:/"openapi"\s*:\s*"(?3\.1\.(?:[1-9]\d*|0))"/},methods:{async canParse(e){const t=0===this.fileExtensions.length||this.fileExtensions.includes(e.extension),n=this.mediaTypes.includes(e.mediaType);if(!t)return!1;if(n)return!0;if(!n)try{const t=e.toString();return JSON.parse(t),this.detectionRegExp.test(t)}catch(e){return!1}return!1},async parse(e){if(this.sourceMap)throw new _b("openapi-json-3-1-swagger-client parser plugin doesn't support sourceMaps option");const t=new zo,n=e.toString();if(this.allowEmpty&&""===n.trim())return t;try{const e=JSON.parse(n),r=km.refract(e,this.refractorOpts);return r.classes.push("result"),t.push(r),t}catch(t){throw new _b(`Error parsing "${e.uri}"`,{cause:t})}}}}),jw=Nb.compose({props:{name:"openapi-yaml-3-1-swagger-client",fileExtensions:[".yaml",".yml"],mediaTypes:new Uv(...zv.filterByFormat("generic"),...zv.filterByFormat("yaml")),detectionRegExp:/(?^(["']?)openapi\2\s*:\s*(["']?)(?3\.1\.(?:[1-9]\d*|0))\3(?:\s+|$))|(?"openapi"\s*:\s*"(?3\.1\.(?:[1-9]\d*|0))")/m},methods:{async canParse(e){const t=0===this.fileExtensions.length||this.fileExtensions.includes(e.extension),n=this.mediaTypes.includes(e.mediaType);if(!t)return!1;if(n)return!0;if(!n)try{const t=e.toString();return le.ZP.load(t),this.detectionRegExp.test(t)}catch(e){return!1}return!1},async parse(e){if(this.sourceMap)throw new _b("openapi-yaml-3-1-swagger-client parser plugin doesn't support sourceMaps option");const t=new zo,n=e.toString();try{const e=le.ZP.load(n,{schema:le.A8});if(this.allowEmpty&&void 0===e)return t;const r=km.refract(e,this.refractorOpts);return r.classes.push("result"),t.push(r),t}catch(t){throw new _b(`Error parsing "${e.uri}"`,{cause:t})}}}}),Ow=Ys({props:{name:null},methods:{canDereference:()=>!1,async dereference(){throw new Cb}}});function kw(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function Aw(e){for(var t=1;t=this.options.resolve.maxDepth)throw new Db(`Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file "${this.reference.uri}"`);const t=this.toBaseURI(e),{refSet:n}=this.reference;if(n.has(t))return n.find(xo(t,"uri"));const r=await qb(ub(t),Aw(Aw({},this.options),{},{parse:Aw(Aw({},this.options.parse),{},{mediaType:"text/plain"})})),o=hb({uri:t,value:r,depth:this.reference.depth+1});return n.add(o),o},async ReferenceElement(e,t,n,r,o){var s,i,a,l,c;const[u,p]=this.toAncestorLineage([...o,n]);if(u.some((t=>t.has(e))))return!1;if(!this.options.resolve.external&&Ug(e))return!1;const h=await this.toReference(null===(s=e.$ref)||void 0===s?void 0:s.toValue()),f=h.uri,d=lb(f,null===(i=e.$ref)||void 0===i?void 0:i.toValue());this.indirections.push(e);const m=Ki(d);let g=Ji(m,h.value.result);if(As(g)){const t=e.meta.get("referenced-element").toValue();if(Uc(g))g=Tm.refract(g),g.setMetaProperty("referenced-element",t);else{g=this.namespace.getElementClass(t).refract(g)}}if(this.indirections.includes(g))throw new Error("Recursive Reference Object detected");if(this.indirections.length>this.options.dereference.maxDepth)throw new Fb(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`);p.add(e);const y=Pw({reference:h,namespace:this.namespace,indirections:[...this.indirections],options:this.options,ancestors:u});g=await Cw(g,y,{keyMap:Tv,nodeTypeGetter:Iv}),p.delete(e),this.indirections.pop(),g=g.clone(),g.setMetaProperty("ref-fields",{$ref:null===(a=e.$ref)||void 0===a?void 0:a.toValue(),description:null===(l=e.description)||void 0===l?void 0:l.toValue(),summary:null===(c=e.summary)||void 0===c?void 0:c.toValue()}),g.setMetaProperty("ref-origin",h.uri);const v=wo(Kv,["description"],e),b=wo(Kv,["summary"],e);return v&&Gr("description",g)&&(g.description=e.description),b&&Gr("summary",g)&&(g.summary=e.summary),this.indirections.pop(),g},async PathItemElement(e,t,n,r,o){var s,i,a;const[l,c]=this.toAncestorLineage([...o,n]);if(!ms(e.$ref))return;if(l.some((t=>t.has(e))))return!1;if(!this.options.resolve.external&&Bg(e))return;const u=await this.toReference(null===(s=e.$ref)||void 0===s?void 0:s.toValue()),p=u.uri,h=lb(p,null===(i=e.$ref)||void 0===i?void 0:i.toValue());this.indirections.push(e);const f=Ki(h);let d=Ji(f,u.value.result);if(As(d)&&(d=Pm.refract(d)),this.indirections.includes(d))throw new Error("Recursive Path Item Object reference detected");if(this.indirections.length>this.options.dereference.maxDepth)throw new Fb(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`);c.add(e);const m=Pw({reference:u,namespace:this.namespace,indirections:[...this.indirections],options:this.options,ancestors:l});d=await Cw(d,m,{keyMap:Tv,nodeTypeGetter:Iv}),c.delete(e),this.indirections.pop();const g=new Pm([...d.content],d.meta.clone(),d.attributes.clone());return e.forEach(((e,t,n)=>{g.remove(t.toValue()),g.content.push(n)})),g.remove("$ref"),g.setMetaProperty("ref-fields",{$ref:null===(a=e.$ref)||void 0===a?void 0:a.toValue()}),g.setMetaProperty("ref-origin",u.uri),g},async LinkElement(e){if(!ms(e.operationRef)&&!ms(e.operationId))return;if(!this.options.resolve.external&&Tg(e))return;if(ms(e.operationRef)&&ms(e.operationId))throw new Error("LinkElement operationRef and operationId fields are mutually exclusive.");let t;if(ms(e.operationRef)){var n,r,o;const s=Ki(null===(n=e.operationRef)||void 0===n?void 0:n.toValue()),i=await this.toReference(null===(r=e.operationRef)||void 0===r?void 0:r.toValue());t=Ji(s,i.value.result),As(t)&&(t=Am.refract(t)),t=new Am([...t.content],t.meta.clone(),t.attributes.clone()),t.setMetaProperty("ref-origin",i.uri),null===(o=e.operationRef)||void 0===o||o.meta.set("operation",t)}else if(ms(e.operationId)){var s,i;const n=null===(s=e.operationId)||void 0===s?void 0:s.toValue();if(t=Vb((e=>Dg(e)&&e.operationId.equals(n)),this.reference.value.result),qo(t))throw new Error(`OperationElement(operationId=${n}) not found.`);null===(i=e.operationId)||void 0===i||i.meta.set("operation",t)}},async ExampleElement(e){var t;if(!ms(e.externalValue))return;if(!this.options.resolve.external&&ms(e.externalValue))return;if(e.hasKey("value")&&ms(e.externalValue))throw new Error("ExampleElement value and externalValue fields are mutually exclusive.");const n=await this.toReference(null===(t=e.externalValue)||void 0===t?void 0:t.toValue()),r=new n.value.result.constructor(n.value.result.content,n.value.result.meta.clone(),n.value.result.attributes.clone());r.setMetaProperty("ref-origin",n.uri),e.value=r},async SchemaElement(e,t,n,r,o){var s;const[i,a]=this.toAncestorLineage([...o,n]);if(!ms(e.$ref))return;if(i.some((t=>t.has(e))))return!1;let{reference:l}=this,{uri:c}=l;const u=Yb(c,e),p=ib(u),h=vb({uri:p}),f=go((e=>e.canRead(h)),this.options.resolve.resolvers),d=!f,m=d&&c!==p;if(!this.options.resolve.external&&m)return;let g;this.indirections.push(e);try{if(f||d){g=ew(u,Qb(l.value.result))}else{l=await this.toReference(ub(u));const e=Ki(u);g=Qb(Ji(e,l.value.result))}}catch(e){if(!(d&&e instanceof zb))throw e;if(Hb(Gb(u))){l=await this.toReference(ub(u)),c=l.uri;const e=Gb(u);g=Zb(e,Qb(l.value.result))}else{l=await this.toReference(ub(u)),c=l.uri;const e=Ki(u);g=Qb(Ji(e,l.value.result))}}if(this.indirections.includes(g))throw new Error("Recursive Schema Object reference detected");if(this.indirections.length>this.options.dereference.maxDepth)throw new Fb(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`);a.add(e);const y=Pw({reference:l,namespace:this.namespace,indirections:[...this.indirections],options:this.options,ancestors:i});if(g=await Cw(g,y,{keyMap:Tv,nodeTypeGetter:Iv}),a.delete(e),this.indirections.pop(),Kg(g)){var v;const t=g.clone();return t.setMetaProperty("ref-fields",{$ref:null===(v=e.$ref)||void 0===v?void 0:v.toValue()}),t.setMetaProperty("ref-origin",l.uri),t}const b=new Lm([...g.content],g.meta.clone(),g.attributes.clone());return e.forEach(((e,t,n)=>{b.remove(t.toValue()),b.content.push(n)})),b.remove("$ref"),b.setMetaProperty("ref-fields",{$ref:null===(s=e.$ref)||void 0===s?void 0:s.toValue()}),b.setMetaProperty("ref-origin",l.uri),b}}}),Nw=Pw,Iw=fi[Symbol.for("nodejs.util.promisify.custom")],Tw=Ys(Ow,{init(){this.name="openapi-3-1"},methods:{canDereference(e){var t;return"text/plain"!==e.mediaType?zv.includes(e.mediaType):Mg(null===(t=e.parseResult)||void 0===t?void 0:t.result)},async dereference(e,t){const n=zs(Rv),r=kr(db(),t.dereference.refSet);let o;r.has(e.uri)?o=r.find(xo(e.uri,"uri")):(o=hb({uri:e.uri,value:e.parseResult}),r.add(o));const s=Nw({reference:o,namespace:n,options:t}),i=await Iw(r.rootRef.value,s,{keyMap:Tv,nodeTypeGetter:Iv});return null===t.dereference.refSet&&r.clean(),i}}}),Rw=Tw,Mw=e=>{const t=(e=>e.slice(2))(e);return t.reduce(((e,n,r)=>{if(Es(n)){const t=String(n.key.toValue());e.push(t)}else if(ws(t[r-2])){const o=t[r-2].content.indexOf(n);e.push(o)}return e}),[])},Dw=e=>{if(null==e.cause)return e;let{cause:t}=e;for(;null!=t.cause;)t=t.cause;return t},Fw=ue("SchemaRefError",(function(e,t,n){this.originalError=n,Object.assign(this,t||{})})),{wrapError:Lw}=ke,Bw=fi[Symbol.for("nodejs.util.promisify.custom")],$w=Nw.compose({props:{useCircularStructures:!0,allowMetaPatches:!1,basePath:null},init(e){let{allowMetaPatches:t=this.allowMetaPatches,useCircularStructures:n=this.useCircularStructures,basePath:r=this.basePath}=e;this.allowMetaPatches=t,this.useCircularStructures=n,this.basePath=r},methods:{async ReferenceElement(e,t,n,r,o){try{const[t,r]=this.toAncestorLineage([...o,n]);if(Ns(["cycle"],e.$ref))return!1;if(t.some((t=>t.has(e))))return!1;if(!this.options.resolve.external&&Ug(e))return!1;const s=await this.toReference(e.$ref.toValue()),i=s.uri,a=lb(i,e.$ref.toValue());this.indirections.push(e);const l=Ki(a);let c=Ji(l,s.value.result);if(As(c)){const t=e.meta.get("referenced-element").toValue();if(Uc(c))c=Tm.refract(c),c.setMetaProperty("referenced-element",t);else{const e=this.namespace.getElementClass(t);c=e.refract(c)}}if(this.indirections.includes(c))throw new Error("Recursive JSON Pointer detected");if(this.indirections.length>this.options.dereference.maxDepth)throw new Fb(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`);if(!this.useCircularStructures){if(t.some((e=>e.has(c)))){if(rb(i)||nb(i)){const t=new Tm({$ref:a},e.meta.clone(),e.attributes.clone());return t.get("$ref").classes.push("cycle"),t}return!1}}r.add(e);const u=$w({reference:s,namespace:this.namespace,indirections:[...this.indirections],options:this.options,ancestors:t,allowMetaPatches:this.allowMetaPatches,useCircularStructures:this.useCircularStructures,basePath:this.basePath??[...Mw([...o,n,e]),"$ref"]});c=await Bw(c,u,{keyMap:Tv,nodeTypeGetter:Iv}),r.delete(e),this.indirections.pop(),c=c.clone(),c.setMetaProperty("ref-fields",{$ref:e.$ref?.toValue(),description:e.description?.toValue(),summary:e.summary?.toValue()}),c.setMetaProperty("ref-origin",s.uri);const p=void 0!==e.description,h=void 0!==e.summary;if(p&&"description"in c&&(c.description=e.description),h&&"summary"in c&&(c.summary=e.summary),this.allowMetaPatches&&bs(c)){const e=c;if(void 0===e.get("$$ref")){const t=lb(i,a);e.set("$$ref",t)}}return c}catch(t){const r=Dw(t),s=Lw(r,{baseDoc:this.reference.uri,$ref:e.$ref.toValue(),pointer:Ki(e.$ref.toValue()),fullPath:this.basePath??[...Mw([...o,n,e]),"$ref"]});return void this.options.dereference.dereferenceOpts?.errors?.push?.(s)}},async PathItemElement(e,t,n,r,o){try{const[t,r]=this.toAncestorLineage([...o,n]);if(!ms(e.$ref))return;if(Ns(["cycle"],e.$ref))return!1;if(t.some((t=>t.has(e))))return!1;if(!this.options.resolve.external&&Bg(e))return;const s=await this.toReference(e.$ref.toValue()),i=s.uri,a=lb(i,e.$ref.toValue());this.indirections.push(e);const l=Ki(a);let c=Ji(l,s.value.result);if(As(c)&&(c=Pm.refract(c)),this.indirections.includes(c))throw new Error("Recursive JSON Pointer detected");if(this.indirections.length>this.options.dereference.maxDepth)throw new Fb(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`);if(!this.useCircularStructures){if(t.some((e=>e.has(c)))){if(rb(i)||nb(i)){const t=new Pm({$ref:a},e.meta.clone(),e.attributes.clone());return t.get("$ref").classes.push("cycle"),t}return!1}}r.add(e);const u=$w({reference:s,namespace:this.namespace,indirections:[...this.indirections],options:this.options,ancestors:t,allowMetaPatches:this.allowMetaPatches,useCircularStructures:this.useCircularStructures,basePath:this.basePath??[...Mw([...o,n,e]),"$ref"]});c=await Bw(c,u,{keyMap:Tv,nodeTypeGetter:Iv}),r.delete(e),this.indirections.pop();const p=new Pm([...c.content],c.meta.clone(),c.attributes.clone());if(e.forEach(((e,t,n)=>{p.remove(t.toValue()),p.content.push(n)})),p.remove("$ref"),p.setMetaProperty("ref-fields",{$ref:e.$ref?.toValue()}),p.setMetaProperty("ref-origin",s.uri),this.allowMetaPatches&&void 0===p.get("$$ref")){const e=lb(i,a);p.set("$$ref",e)}return p}catch(t){const r=Dw(t),s=Lw(r,{baseDoc:this.reference.uri,$ref:e.$ref.toValue(),pointer:Ki(e.$ref.toValue()),fullPath:this.basePath??[...Mw([...o,n,e]),"$ref"]});return void this.options.dereference.dereferenceOpts?.errors?.push?.(s)}},async SchemaElement(e,t,n,r,o){try{const[t,r]=this.toAncestorLineage([...o,n]);if(!ms(e.$ref))return;if(Ns(["cycle"],e.$ref))return!1;if(t.some((t=>t.has(e))))return!1;let{reference:s}=this,{uri:i}=s;const a=Yb(i,e),l=ib(a),c=vb({uri:l}),u=!this.options.resolve.resolvers.some((e=>e.canRead(c))),p=!u,h=p&&i!==l;if(!this.options.resolve.external&&h)return;let f;this.indirections.push(e);try{if(u||p){f=ew(a,Qb(s.value.result))}else{s=await this.toReference(ub(a)),i=s.uri;const e=Ki(a);f=Qb(Ji(e,s.value.result))}}catch(e){if(!(p&&e instanceof zb))throw e;if(Hb(Gb(a))){s=await this.toReference(ub(a)),i=s.uri;const e=Gb(a);f=Zb(e,Qb(s.value.result))}else{s=await this.toReference(ub(a)),i=s.uri;const e=Ki(a);f=Qb(Ji(e,s.value.result))}}if(this.indirections.includes(f))throw new Error("Recursive Schema Object reference detected");if(this.indirections.length>this.options.dereference.maxDepth)throw new Fb(`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`);if(!this.useCircularStructures){if(t.some((e=>e.has(f)))){if(rb(i)||nb(i)){const t=lb(i,a),n=new Lm({$ref:t},e.meta.clone(),e.attributes.clone());return n.get("$ref").classes.push("cycle"),n}return!1}}r.add(e);const d=$w({reference:s,namespace:this.namespace,indirections:[...this.indirections],options:this.options,useCircularStructures:this.useCircularStructures,allowMetaPatches:this.allowMetaPatches,ancestors:t,basePath:this.basePath??[...Mw([...o,n,e]),"$ref"]});if(f=await Bw(f,d,{keyMap:Tv,nodeTypeGetter:Iv}),r.delete(e),this.indirections.pop(),Kg(f)){const t=f.clone();return t.setMetaProperty("ref-fields",{$ref:e.$ref?.toValue()}),t.setMetaProperty("ref-origin",i),t}const m=new Lm([...f.content],f.meta.clone(),f.attributes.clone());if(e.forEach(((e,t,n)=>{m.remove(t.toValue()),m.content.push(n)})),m.remove("$ref"),m.setMetaProperty("ref-fields",{$ref:e.$ref?.toValue()}),m.setMetaProperty("ref-origin",i),this.allowMetaPatches&&void 0===m.get("$$ref")){const e=lb(i,a);m.set("$$ref",e)}return m}catch(t){const r=Dw(t),s=new Fw(`Could not resolve reference: ${r.message}`,{baseDoc:this.reference.uri,$ref:e.$ref.toValue(),fullPath:this.basePath??[...Mw([...o,n,e]),"$ref"]},r);return void this.options.dereference.dereferenceOpts?.errors?.push?.(s)}},async LinkElement(){},async ExampleElement(e,t,n,r,o){try{return await Nw.compose.methods.ExampleElement.call(this,e,t,n,r,o)}catch(t){const r=Dw(t),s=Lw(r,{baseDoc:this.reference.uri,externalValue:e.externalValue?.toValue(),fullPath:this.basePath??[...Mw([...o,n,e]),"externalValue"]});return void this.options.dereference.dereferenceOpts?.errors?.push?.(s)}}}}),qw=$w,Uw=Rw.compose.bind(),zw=Uw({init(e){let{parameterMacro:t,options:n}=e;this.parameterMacro=t,this.options=n},props:{parameterMacro:null,options:null,macroOperation:null,OperationElement:{enter(e){this.macroOperation=e},leave(){this.macroOperation=null}},ParameterElement:{leave(e,t,n,r,o){const s=null===this.macroOperation?null:Ti(this.macroOperation),i=Ti(e);try{const t=this.parameterMacro(s,i);e.set("default",t)}catch(e){const t=new Error(e,{cause:e});t.fullPath=Mw([...o,n]),this.options.dereference.dereferenceOpts?.errors?.push?.(t)}}}}}),Vw=Uw({init(e){let{modelPropertyMacro:t,options:n}=e;this.modelPropertyMacro=t,this.options=n},props:{modelPropertyMacro:null,options:null,SchemaElement:{leave(e,t,n,r,o){void 0!==e.properties&&bs(e.properties)&&e.properties.forEach((t=>{if(bs(t))try{const e=this.modelPropertyMacro(Ti(t));t.set("default",e)}catch(t){const r=new Error(t,{cause:t});r.fullPath=[...Mw([...o,n,e]),"properties"],this.options.dereference.dereferenceOpts?.errors?.push?.(r)}}))}}}});function Ww(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function Jw(e){for(var t=1;t{const t=e.meta.clone(),n=e.attributes.clone();return new e.constructor(void 0,t,n)},Hw=e=>new Pt.c6(e.key,e.value,e.meta.clone(),e.attributes.clone()),Gw=(e,t)=>t.clone&&t.isMergeableElement(e)?Xw(Kw(e),e,t):e,Zw=(e,t,n)=>e.concat(t)["fantasy-land/map"]((e=>Gw(e,n))),Yw=(e,t,n)=>{const r=bs(e)?Kw(e):Kw(t);return bs(e)&&e.forEach(((e,t,o)=>{const s=Hw(o);s.value=Gw(e,n),r.content.push(s)})),t.forEach(((t,o,s)=>{const i=o.toValue();let a;if(bs(e)&&e.hasKey(i)&&n.isMergeableElement(t)){const r=e.get(i);a=Hw(s),a.value=((e,t)=>{if("function"!=typeof t.customMerge)return Xw;const n=t.customMerge(e,t);return"function"==typeof n?n:Xw})(o,n)(r,t)}else a=Hw(s),a.value=Gw(t,n);r.remove(i),r.content.push(a)})),r};function Xw(e,t,n){var r,o,s;const i={clone:!0,isMergeableElement:e=>bs(e)||ws(e),arrayElementMerge:Zw,objectElementMerge:Yw,customMerge:void 0},a=Jw(Jw({},i),n);a.isMergeableElement=null!==(r=a.isMergeableElement)&&void 0!==r?r:i.isMergeableElement,a.arrayElementMerge=null!==(o=a.arrayElementMerge)&&void 0!==o?o:i.arrayElementMerge,a.objectElementMerge=null!==(s=a.objectElementMerge)&&void 0!==s?s:i.objectElementMerge;const l=ws(t);return l===ws(e)?l&&"function"==typeof a.arrayElementMerge?a.arrayElementMerge(e,t,a):a.objectElementMerge(e,t,a):Gw(t,a)}Xw.all=(e,t)=>{if(!Array.isArray(e))throw new Error("first argument should be an array");return 0===e.length?new Pt.Sb:e.reduce(((e,n)=>Xw(e,n,t)),Kw(e[0]))};const Qw=Uw({init(e){let{options:t}=e;this.options=t},props:{options:null,SchemaElement:{leave(e,t,n,r,o){if(void 0===e.allOf)return;if(!ws(e.allOf)){const t=new TypeError("allOf must be an array");return t.fullPath=[...Mw([...o,n,e]),"allOf"],void this.options.dereference.dereferenceOpts?.errors?.push?.(t)}if(e.allOf.isEmpty)return new Lm(e.content.filter((e=>"allOf"!==e.key.toValue())),e.meta.clone(),e.attributes.clone());if(!e.allOf.content.every(Jg)){const t=new TypeError("Elements in allOf must be objects");return t.fullPath=[...Mw([...o,n,e]),"allOf"],void this.options.dereference.dereferenceOpts?.errors?.push?.(t)}const s=Xw.all([...e.allOf.content,e]);if(e.hasKey("$$ref")||s.remove("$$ref"),e.hasKey("example")){s.getMember("example").value=e.get("example")}if(e.hasKey("examples")){s.getMember("examples").value=e.get("examples")}return s.remove("allOf"),s}}}}),eE=fi[Symbol.for("nodejs.util.promisify.custom")],tE=Rw.compose({props:{useCircularStructures:!0,allowMetaPatches:!1,parameterMacro:null,modelPropertyMacro:null,mode:"non-strict",ancestors:null},init(){let{useCircularStructures:e=this.useCircularStructures,allowMetaPatches:t=this.allowMetaPatches,parameterMacro:n=this.parameterMacro,modelPropertyMacro:r=this.modelPropertyMacro,mode:o=this.mode,ancestors:s=[]}=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};this.name="openapi-3-1-swagger-client",this.useCircularStructures=e,this.allowMetaPatches=t,this.parameterMacro=n,this.modelPropertyMacro=r,this.mode=o,this.ancestors=[...s]},methods:{async dereference(e,t){const n=[],r=zs(Rv),o=t.dereference.refSet??db();let s;o.has(e.uri)?s=o.find((t=>t.uri===e.uri)):(s=hb({uri:e.uri,value:e.parseResult}),o.add(s));const i=qw({reference:s,namespace:r,options:t,useCircularStructures:this.useCircularStructures,allowMetaPatches:this.allowMetaPatches,ancestors:this.ancestors});if(n.push(i),"function"==typeof this.parameterMacro){const e=zw({parameterMacro:this.parameterMacro,options:t});n.push(e)}if("function"==typeof this.modelPropertyMacro){const e=Vw({modelPropertyMacro:this.modelPropertyMacro,options:t});n.push(e)}if("strict"!==this.mode){const e=Qw({options:t});n.push(e)}const a=ri(n,{nodeTypeGetter:Iv}),l=await eE(o.rootRef.value,a,{keyMap:Tv,nodeTypeGetter:Iv});return null===t.dereference.refSet&&o.clean(),l}}}),nE=tE,rE=async e=>{const{spec:t,timeout:n,redirects:r,requestInterceptor:o,responseInterceptor:s,pathDiscriminator:i=[],allowMetaPatches:a=!1,useCircularStructures:l=!1,skipNormalization:c=!1,parameterMacro:u=null,modelPropertyMacro:p=null,mode:h="non-strict"}=e;try{const{cache:d}=rE,m=rb(ab())?ab():"https://smartbear.com/",g=Et(e),y=lb(m,g);let v;d.has(t)?v=d.get(t):(v=km.refract(t),v.classes.push("result"),d.set(t,v));const b=new zo([v]),w=0===(f=i).length?"":`/${f.map(Vi).join("/")}`,E=""===w?"":`#${w}`,x=Ji(w,v),S=hb({uri:y,value:b}),_=db({refs:[S]});""!==w&&(_.rootRef=null);const j=[new WeakSet([x])],O=[],k=((e,t,n)=>Ei({element:n}).transclude(e,t))(x,await Ab(x,{resolve:{baseURI:`${y}${E}`,resolvers:[Ew({timeout:n||1e4,redirects:r||10})],resolverOpts:{swaggerHTTPClientConfig:{requestInterceptor:o,responseInterceptor:s}},strategies:[lw()]},parse:{mediaType:zv.latest(),parsers:[_w({allowEmpty:!1,sourceMap:!1}),jw({allowEmpty:!1,sourceMap:!1}),xw({allowEmpty:!1,sourceMap:!1}),Sw({allowEmpty:!1,sourceMap:!1}),Ib({allowEmpty:!1,sourceMap:!1})]},dereference:{maxDepth:100,strategies:[nE({allowMetaPatches:a,useCircularStructures:l,parameterMacro:u,modelPropertyMacro:p,mode:h,ancestors:j})],refSet:_,dereferenceOpts:{errors:O}}}),v),A=c?k:bw(k);return{spec:Ti(A),errors:O}}catch(e){if(e instanceof Ui||e instanceof zi)return{spec:null,errors:[]};throw e}var f};rE.cache=new WeakMap;const oE=rE,sE={name:"openapi-3-1-apidom",match(e){let{spec:t}=e;return Ot(t)},normalize(e){let{spec:t}=e;return vw(bw)(t)},resolve:async e=>oE(e)},iE=e=>async t=>(async e=>{const{spec:t,requestInterceptor:n,responseInterceptor:r}=e,o=Et(e),s=xt(e),i=t||await Ze(s,{requestInterceptor:n,responseInterceptor:r})(o),a=f()(f()({},e),{},{spec:i});return e.strategies.find((e=>e.match(a))).resolve(a)})(f()(f()({},e),t)),aE=iE({strategies:[Ct,At,_t]});var lE=n(88436),cE=n.n(lE),uE=n(27361),pE=n.n(uE),hE=n(76489);function fE(e){return"[object Object]"===Object.prototype.toString.call(e)}function dE(e){var t,n;return!1!==fE(e)&&(void 0===(t=e.constructor)||!1!==fE(n=t.prototype)&&!1!==n.hasOwnProperty("isPrototypeOf"))}const mE={body:function(e){let{req:t,value:n}=e;t.body=n},header:function(e){let{req:t,parameter:n,value:r}=e;t.headers=t.headers||{},void 0!==r&&(t.headers[n.name]=r)},query:function(e){let{req:t,value:n,parameter:r}=e;t.query=t.query||{},!1===n&&"boolean"===r.type&&(n="false");0===n&&["number","integer"].indexOf(r.type)>-1&&(n="0");if(n)t.query[r.name]={collectionFormat:r.collectionFormat,value:n};else if(r.allowEmptyValue&&void 0!==n){const e=r.name;t.query[e]=t.query[e]||{},t.query[e].allowEmptyValue=!0}},path:function(e){let{req:t,value:n,parameter:r}=e;t.url=t.url.split(`{${r.name}}`).join(encodeURIComponent(n))},formData:function(e){let{req:t,value:n,parameter:r}=e;(n||r.allowEmptyValue)&&(t.form=t.form||{},t.form[r.name]={value:n,allowEmptyValue:r.allowEmptyValue,collectionFormat:r.collectionFormat})}};function gE(e,t){return t.includes("application/json")?"string"==typeof e?e:JSON.stringify(e):e.toString()}function yE(e){let{req:t,value:n,parameter:r}=e;const{name:o,style:s,explode:i,content:a}=r;if(a){const e=Object.keys(a)[0];return void(t.url=t.url.split(`{${o}}`).join(st(gE(n,e),{escape:!0})))}const l=it({key:r.name,value:n,style:s||"simple",explode:i||!1,escape:!0});t.url=t.url.split(`{${o}}`).join(l)}function vE(e){let{req:t,value:n,parameter:r}=e;if(t.query=t.query||{},r.content){const e=gE(n,Object.keys(r.content)[0]);if(e)t.query[r.name]=e;else if(r.allowEmptyValue&&void 0!==n){const e=r.name;t.query[e]=t.query[e]||{},t.query[e].allowEmptyValue=!0}}else if(!1===n&&(n="false"),0===n&&(n="0"),n){const{style:e,explode:o,allowReserved:s}=r;t.query[r.name]={value:n,serializationOption:{style:e,explode:o,allowReserved:s}}}else if(r.allowEmptyValue&&void 0!==n){const e=r.name;t.query[e]=t.query[e]||{},t.query[e].allowEmptyValue=!0}}const bE=["accept","authorization","content-type"];function wE(e){let{req:t,parameter:n,value:r}=e;if(t.headers=t.headers||{},!(bE.indexOf(n.name.toLowerCase())>-1))if(n.content){const e=Object.keys(n.content)[0];t.headers[n.name]=gE(r,e)}else void 0!==r&&(t.headers[n.name]=it({key:n.name,value:r,style:n.style||"simple",explode:void 0!==n.explode&&n.explode,escape:!1}))}function EE(e){let{req:t,parameter:n,value:r}=e;t.headers=t.headers||{};const o=typeof r;if(n.content){const e=Object.keys(n.content)[0];t.headers.Cookie=`${n.name}=${gE(r,e)}`}else if("undefined"!==o){const e="object"===o&&!Array.isArray(r)&&n.explode?"":`${n.name}=`;t.headers.Cookie=e+it({key:n.name,value:r,escape:!1,style:n.style||"form",explode:void 0!==n.explode&&n.explode})}}const xE="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:window,{btoa:SE}=xE,_E=SE;function jE(e,t){const{operation:n,requestBody:r,securities:o,spec:s,attachContentTypeForEmptyPayload:i}=e;let{requestContentType:a}=e;t=function(e){let{request:t,securities:n={},operation:r={},spec:o}=e;const s=f()({},t),{authorized:i={}}=n,a=r.security||o.security||[],l=i&&!!Object.keys(i).length,c=pE()(o,["components","securitySchemes"])||{};if(s.headers=s.headers||{},s.query=s.query||{},!Object.keys(n).length||!l||!a||Array.isArray(r.security)&&!r.security.length)return t;return a.forEach((e=>{Object.keys(e).forEach((e=>{const t=i[e],n=c[e];if(!t)return;const r=t.value||t,{type:o}=n;if(t)if("apiKey"===o)"query"===n.in&&(s.query[n.name]=r),"header"===n.in&&(s.headers[n.name]=r),"cookie"===n.in&&(s.cookies[n.name]=r);else if("http"===o){if(/^basic$/i.test(n.scheme)){const e=r.username||"",t=r.password||"",n=_E(`${e}:${t}`);s.headers.Authorization=`Basic ${n}`}/^bearer$/i.test(n.scheme)&&(s.headers.Authorization=`Bearer ${r}`)}else if("oauth2"===o||"openIdConnect"===o){const e=t.token||{},r=e[n["x-tokenName"]||"access_token"];let o=e.token_type;o&&"bearer"!==o.toLowerCase()||(o="Bearer"),s.headers.Authorization=`${o} ${r}`}}))})),s}({request:t,securities:o,operation:n,spec:s});const l=n.requestBody||{},c=Object.keys(l.content||{}),u=a&&c.indexOf(a)>-1;if(r||i){if(a&&u)t.headers["Content-Type"]=a;else if(!a){const e=c[0];e&&(t.headers["Content-Type"]=e,a=e)}}else a&&u&&(t.headers["Content-Type"]=a);if(!e.responseContentType&&n.responses){const e=Object.entries(n.responses).filter((e=>{let[t,n]=e;const r=parseInt(t,10);return r>=200&&r<300&&dE(n.content)})).reduce(((e,t)=>{let[,n]=t;return e.concat(Object.keys(n.content))}),[]);e.length>0&&(t.headers.accept=e.join(", "))}if(r)if(a){if(c.indexOf(a)>-1)if("application/x-www-form-urlencoded"===a||"multipart/form-data"===a)if("object"==typeof r){const e=(l.content[a]||{}).encoding||{};t.form={},Object.keys(r).forEach((n=>{t.form[n]={value:r[n],encoding:e[n]||{}}}))}else t.form=r;else t.body=r}else t.body=r;return t}function OE(e,t){const{spec:n,operation:r,securities:o,requestContentType:s,responseContentType:i,attachContentTypeForEmptyPayload:a}=e;if(t=function(e){let{request:t,securities:n={},operation:r={},spec:o}=e;const s=f()({},t),{authorized:i={},specSecurity:a=[]}=n,l=r.security||a,c=i&&!!Object.keys(i).length,u=o.securityDefinitions;if(s.headers=s.headers||{},s.query=s.query||{},!Object.keys(n).length||!c||!l||Array.isArray(r.security)&&!r.security.length)return t;return l.forEach((e=>{Object.keys(e).forEach((e=>{const t=i[e];if(!t)return;const{token:n}=t,r=t.value||t,o=u[e],{type:a}=o,l=o["x-tokenName"]||"access_token",c=n&&n[l];let p=n&&n.token_type;if(t)if("apiKey"===a){const e="query"===o.in?"query":"headers";s[e]=s[e]||{},s[e][o.name]=r}else if("basic"===a)if(r.header)s.headers.authorization=r.header;else{const e=r.username||"",t=r.password||"";r.base64=_E(`${e}:${t}`),s.headers.authorization=`Basic ${r.base64}`}else"oauth2"===a&&c&&(p=p&&"bearer"!==p.toLowerCase()?p:"Bearer",s.headers.authorization=`${p} ${c}`)}))})),s}({request:t,securities:o,operation:r,spec:n}),t.body||t.form||a)s?t.headers["Content-Type"]=s:Array.isArray(r.consumes)?[t.headers["Content-Type"]]=r.consumes:Array.isArray(n.consumes)?[t.headers["Content-Type"]]=n.consumes:r.parameters&&r.parameters.filter((e=>"file"===e.type)).length?t.headers["Content-Type"]="multipart/form-data":r.parameters&&r.parameters.filter((e=>"formData"===e.in)).length&&(t.headers["Content-Type"]="application/x-www-form-urlencoded");else if(s){const e=r.parameters&&r.parameters.filter((e=>"body"===e.in)).length>0,n=r.parameters&&r.parameters.filter((e=>"formData"===e.in)).length>0;(e||n)&&(t.headers["Content-Type"]=s)}return!i&&Array.isArray(r.produces)&&r.produces.length>0&&(t.headers.accept=r.produces.join(", ")),t}function kE(e,t){return`${t.toLowerCase()}-${e}`}const AE=["http","fetch","spec","operationId","pathName","method","parameters","securities"],CE=e=>Array.isArray(e)?e:[],PE=ue("OperationNotFoundError",(function(e,t,n){this.originalError=n,Object.assign(this,t||{})})),NE=(e,t)=>t.filter((t=>t.name===e)),IE=e=>{const t={};e.forEach((e=>{t[e.in]||(t[e.in]={}),t[e.in][e.name]=e}));const n=[];return Object.keys(t).forEach((e=>{Object.keys(t[e]).forEach((r=>{n.push(t[e][r])}))})),n},TE={buildRequest:ME};function RE(e){let{http:t,fetch:n,spec:r,operationId:o,pathName:s,method:i,parameters:a,securities:l}=e,c=cE()(e,AE);const u=t||n||ct;s&&i&&!o&&(o=kE(s,i));const p=TE.buildRequest(f()({spec:r,operationId:o,parameters:a,securities:l,http:u},c));return p.body&&(dE(p.body)||Array.isArray(p.body))&&(p.body=JSON.stringify(p.body)),u(p)}function ME(e){const{spec:t,operationId:n,responseContentType:r,scheme:o,requestInterceptor:s,responseInterceptor:i,contextUrl:a,userFetch:l,server:c,serverVariables:p,http:h,signal:d}=e;let{parameters:m,parameterBuilders:g}=e;const y=kt(t);g||(g=y?u:mE);let v={url:"",credentials:h&&h.withCredentials?"include":"same-origin",headers:{},cookies:{}};d&&(v.signal=d),s&&(v.requestInterceptor=s),i&&(v.responseInterceptor=i),l&&(v.userFetch=l);const b=function(e,t){return e&&e.paths?function(e,t){return function(e,t,n){if(!e||"object"!=typeof e||!e.paths||"object"!=typeof e.paths)return null;const{paths:r}=e;for(const o in r)for(const s in r[o]){if("PARAMETERS"===s.toUpperCase())continue;const i=r[o][s];if(!i||"object"!=typeof i)continue;const a={spec:e,pathName:o,method:s.toUpperCase(),operation:i},l=t(a);if(n&&l)return a}}(e,t,!0)||null}(e,(e=>{let{pathName:n,method:r,operation:o}=e;if(!o||"object"!=typeof o)return!1;const s=o.operationId;return[(0,He.Z)(o,n,r),kE(n,r),s].some((e=>e&&e===t))})):null}(t,n);if(!b)throw new PE(`Operation ${n} not found`);const{operation:w={},method:E,pathName:x}=b;if(v.url+=function(e){const t=kt(e.spec);return t?function(e){let{spec:t,pathName:n,method:r,server:o,contextUrl:s,serverVariables:i={}}=e;const a=pE()(t,["paths",n,(r||"").toLowerCase(),"servers"])||pE()(t,["paths",n,"servers"])||pE()(t,["servers"]);let l="",c=null;if(o&&a&&a.length){const e=a.map((e=>e.url));e.indexOf(o)>-1&&(l=o,c=a[e.indexOf(o)])}!l&&a&&a.length&&(l=a[0].url,[c]=a);if(l.indexOf("{")>-1){(function(e){const t=[],n=/{([^}]+)}/g;let r;for(;r=n.exec(e);)t.push(r[1]);return t})(l).forEach((e=>{if(c.variables&&c.variables[e]){const t=c.variables[e],n=i[e]||t.default,r=new RegExp(`{${e}}`,"g");l=l.replace(r,n)}}))}return function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";const n=e&&t?ce.parse(ce.resolve(t,e)):ce.parse(e),r=ce.parse(t),o=DE(n.protocol)||DE(r.protocol)||"",s=n.host||r.host,i=n.pathname||"";let a;a=o&&s?`${o}://${s+i}`:i;return"/"===a[a.length-1]?a.slice(0,-1):a}(l,s)}(e):function(e){let{spec:t,scheme:n,contextUrl:r=""}=e;const o=ce.parse(r),s=Array.isArray(t.schemes)?t.schemes[0]:null,i=n||s||DE(o.protocol)||"http",a=t.host||o.host||"",l=t.basePath||"";let c;c=i&&a?`${i}://${a+l}`:l;return"/"===c[c.length-1]?c.slice(0,-1):c}(e)}({spec:t,scheme:o,contextUrl:a,server:c,serverVariables:p,pathName:x,method:E}),!n)return delete v.cookies,v;v.url+=x,v.method=`${E}`.toUpperCase(),m=m||{};const S=t.paths[x]||{};r&&(v.headers.accept=r);const _=IE([].concat(CE(w.parameters)).concat(CE(S.parameters)));_.forEach((e=>{const n=g[e.in];let r;if("body"===e.in&&e.schema&&e.schema.properties&&(r=m),r=e&&e.name&&m[e.name],void 0===r?r=e&&e.name&&m[`${e.in}.${e.name}`]:NE(e.name,_).length>1&&console.warn(`Parameter '${e.name}' is ambiguous because the defined spec has more than one parameter with the name: '${e.name}' and the passed-in parameter values did not define an 'in' value.`),null!==r){if(void 0!==e.default&&void 0===r&&(r=e.default),void 0===r&&e.required&&!e.allowEmptyValue)throw new Error(`Required parameter ${e.name} is not provided`);if(y&&e.schema&&"object"===e.schema.type&&"string"==typeof r)try{r=JSON.parse(r)}catch(e){throw new Error("Could not parse object parameter value string as JSON")}n&&n({req:v,parameter:e,value:r,operation:w,spec:t})}}));const j=f()(f()({},e),{},{operation:w});if(v=y?jE(j,v):OE(j,v),v.cookies&&Object.keys(v.cookies).length){const e=Object.keys(v.cookies).reduce(((e,t)=>{const n=v.cookies[t];return e+(e?"&":"")+hE.serialize(t,n)}),"");v.headers.Cookie=e}return v.cookies&&delete v.cookies,wt(v),v}const DE=e=>e?e.replace(/\W/g,""):null;const FE=e=>async function(t,n){let r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return async function(e,t){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const{returnEntireTree:r,baseDoc:o,requestInterceptor:s,responseInterceptor:i,parameterMacro:a,modelPropertyMacro:l,useCircularStructures:c,strategies:u}=n,p={spec:e,pathDiscriminator:t,baseDoc:o,requestInterceptor:s,responseInterceptor:i,parameterMacro:a,modelPropertyMacro:l,useCircularStructures:c,strategies:u},h=u.find((e=>e.match(p))).normalize(p),d=await aE(f()(f()({},p),{},{spec:h,allowMetaPatches:!0,skipNormalization:!0}));return!r&&Array.isArray(t)&&t.length&&(d.spec=pE()(d.spec,t)||null),d}(t,n,f()(f()({},e),r))};FE({strategies:[Ct,At,_t]});var LE=n(34852);function BE(e){let{configs:t,getConfigs:n}=e;return{fn:{fetch:(r=ct,o=t.preFetch,s=t.postFetch,s=s||(e=>e),o=o||(e=>e),e=>("string"==typeof e&&(e={url:e}),lt.mergeInQueryOrForm(e),e=o(e),s(r(e)))),buildRequest:ME,execute:RE,resolve:iE({strategies:[sE,Ct,At,_t]}),resolveSubtree:async function(e,t){let r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const o=n(),s={modelPropertyMacro:o.modelPropertyMacro,parameterMacro:o.parameterMacro,requestInterceptor:o.requestInterceptor,responseInterceptor:o.responseInterceptor,strategies:[sE,Ct,At,_t]};return FE(s)(e,t,r)},serializeRes:pt,opId:He.Z},statePlugins:{configs:{wrapActions:{loaded:LE.loaded}}}};var r,o,s}},98525:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(90242);function o(){return{fn:{shallowEqualKeys:r.be}}}},48347:(e,t,n)=>{"use strict";n.r(t),n.d(t,{getDisplayName:()=>r});const r=e=>e.displayName||e.name||"Component"},73420:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>c});var r=n(35627),o=n.n(r),s=n(90242),i=n(11092),a=n(48347),l=n(60314);const c=e=>{let{getComponents:t,getStore:n,getSystem:r}=e;const c=(u=(0,i.getComponent)(r,n,t),(0,s.HP)(u,(function(){for(var e=arguments.length,t=new Array(e),n=0;n(0,l.Z)(e,(function(){for(var e=arguments.length,t=new Array(e),n=0;n{"use strict";n.r(t),n.d(t,{getComponent:()=>X,render:()=>Y,withMappedContainer:()=>Z});var r=n(23101),o=n.n(r),s=n(28222),i=n.n(s),a=n(67294),l=n(73935),c=n(97779),u=n(61688),p=n(52798);let h=function(e){e()};const f=()=>h,d=(0,a.createContext)(null);let m=null;var g=n(87462),y=n(63366),v=n(8679),b=n.n(v),w=n(59864);const E=["initMapStateToProps","initMapDispatchToProps","initMergeProps"];function x(e,t,n,r,{areStatesEqual:o,areOwnPropsEqual:s,areStatePropsEqual:i}){let a,l,c,u,p,h=!1;function f(h,f){const d=!s(f,l),m=!o(h,a,f,l);return a=h,l=f,d&&m?(c=e(a,l),t.dependsOnOwnProps&&(u=t(r,l)),p=n(c,u,l),p):d?(e.dependsOnOwnProps&&(c=e(a,l)),t.dependsOnOwnProps&&(u=t(r,l)),p=n(c,u,l),p):m?function(){const t=e(a,l),r=!i(t,c);return c=t,r&&(p=n(c,u,l)),p}():p}return function(o,s){return h?f(o,s):(a=o,l=s,c=e(a,l),u=t(r,l),p=n(c,u,l),h=!0,p)}}function S(e){return function(t){const n=e(t);function r(){return n}return r.dependsOnOwnProps=!1,r}}function _(e){return e.dependsOnOwnProps?Boolean(e.dependsOnOwnProps):1!==e.length}function j(e,t){return function(t,{displayName:n}){const r=function(e,t){return r.dependsOnOwnProps?r.mapToProps(e,t):r.mapToProps(e,void 0)};return r.dependsOnOwnProps=!0,r.mapToProps=function(t,n){r.mapToProps=e,r.dependsOnOwnProps=_(e);let o=r(t,n);return"function"==typeof o&&(r.mapToProps=o,r.dependsOnOwnProps=_(o),o=r(t,n)),o},r}}function O(e,t){return(n,r)=>{throw new Error(`Invalid value of type ${typeof e} for ${t} argument when connecting component ${r.wrappedComponentName}.`)}}function k(e,t,n){return(0,g.Z)({},n,e,t)}const A={notify(){},get:()=>[]};function C(e,t){let n,r=A;function o(){i.onStateChange&&i.onStateChange()}function s(){n||(n=t?t.addNestedSub(o):e.subscribe(o),r=function(){const e=f();let t=null,n=null;return{clear(){t=null,n=null},notify(){e((()=>{let e=t;for(;e;)e.callback(),e=e.next}))},get(){let e=[],n=t;for(;n;)e.push(n),n=n.next;return e},subscribe(e){let r=!0,o=n={callback:e,next:null,prev:n};return o.prev?o.prev.next=o:t=o,function(){r&&null!==t&&(r=!1,o.next?o.next.prev=o.prev:n=o.prev,o.prev?o.prev.next=o.next:t=o.next)}}}}())}const i={addNestedSub:function(e){return s(),r.subscribe(e)},notifyNestedSubs:function(){r.notify()},handleChangeWrapper:o,isSubscribed:function(){return Boolean(n)},trySubscribe:s,tryUnsubscribe:function(){n&&(n(),n=void 0,r.clear(),r=A)},getListeners:()=>r};return i}const P=!("undefined"==typeof window||void 0===window.document||void 0===window.document.createElement)?a.useLayoutEffect:a.useEffect;function N(e,t){return e===t?0!==e||0!==t||1/e==1/t:e!=e&&t!=t}function I(e,t){if(N(e,t))return!0;if("object"!=typeof e||null===e||"object"!=typeof t||null===t)return!1;const n=Object.keys(e),r=Object.keys(t);if(n.length!==r.length)return!1;for(let r=0;r{throw new Error("uSES not initialized!")};const M=[null,null];function D(e,t,n,r,o,s){e.current=r,n.current=!1,o.current&&(o.current=null,s())}function F(e,t){return e===t}const L=function(e,t,n,{pure:r,areStatesEqual:o=F,areOwnPropsEqual:s=I,areStatePropsEqual:i=I,areMergedPropsEqual:l=I,forwardRef:c=!1,context:u=d}={}){const p=u,h=function(e){return e?"function"==typeof e?j(e):O(e,"mapStateToProps"):S((()=>({})))}(e),f=function(e){return e&&"object"==typeof e?S((t=>function(e,t){const n={};for(const r in e){const o=e[r];"function"==typeof o&&(n[r]=(...e)=>t(o(...e)))}return n}(e,t))):e?"function"==typeof e?j(e):O(e,"mapDispatchToProps"):S((e=>({dispatch:e})))}(t),m=function(e){return e?"function"==typeof e?function(e){return function(t,{displayName:n,areMergedPropsEqual:r}){let o,s=!1;return function(t,n,i){const a=e(t,n,i);return s?r(a,o)||(o=a):(s=!0,o=a),o}}}(e):O(e,"mergeProps"):()=>k}(n),v=Boolean(e);return e=>{const t=e.displayName||e.name||"Component",n=`Connect(${t})`,r={shouldHandleStateChanges:v,displayName:n,wrappedComponentName:t,WrappedComponent:e,initMapStateToProps:h,initMapDispatchToProps:f,initMergeProps:m,areStatesEqual:o,areStatePropsEqual:i,areOwnPropsEqual:s,areMergedPropsEqual:l};function u(t){const[n,o,s]=(0,a.useMemo)((()=>{const{reactReduxForwardedRef:e}=t,n=(0,y.Z)(t,T);return[t.context,e,n]}),[t]),i=(0,a.useMemo)((()=>n&&n.Consumer&&(0,w.isContextConsumer)(a.createElement(n.Consumer,null))?n:p),[n,p]),l=(0,a.useContext)(i),c=Boolean(t.store)&&Boolean(t.store.getState)&&Boolean(t.store.dispatch),u=Boolean(l)&&Boolean(l.store);const h=c?t.store:l.store,f=u?l.getServerState:h.getState,d=(0,a.useMemo)((()=>function(e,t){let{initMapStateToProps:n,initMapDispatchToProps:r,initMergeProps:o}=t,s=(0,y.Z)(t,E);return x(n(e,s),r(e,s),o(e,s),e,s)}(h.dispatch,r)),[h]),[m,b]=(0,a.useMemo)((()=>{if(!v)return M;const e=C(h,c?void 0:l.subscription),t=e.notifyNestedSubs.bind(e);return[e,t]}),[h,c,l]),S=(0,a.useMemo)((()=>c?l:(0,g.Z)({},l,{subscription:m})),[c,l,m]),_=(0,a.useRef)(),j=(0,a.useRef)(s),O=(0,a.useRef)(),k=(0,a.useRef)(!1),A=((0,a.useRef)(!1),(0,a.useRef)(!1)),N=(0,a.useRef)();P((()=>(A.current=!0,()=>{A.current=!1})),[]);const I=(0,a.useMemo)((()=>()=>O.current&&s===j.current?O.current:d(h.getState(),s)),[h,s]),F=(0,a.useMemo)((()=>e=>m?function(e,t,n,r,o,s,i,a,l,c,u){if(!e)return()=>{};let p=!1,h=null;const f=()=>{if(p||!a.current)return;const e=t.getState();let n,f;try{n=r(e,o.current)}catch(e){f=e,h=e}f||(h=null),n===s.current?i.current||c():(s.current=n,l.current=n,i.current=!0,u())};return n.onStateChange=f,n.trySubscribe(),f(),()=>{if(p=!0,n.tryUnsubscribe(),n.onStateChange=null,h)throw h}}(v,h,m,d,j,_,k,A,O,b,e):()=>{}),[m]);var L,B,$;let q;L=D,B=[j,_,k,s,O,b],P((()=>L(...B)),$);try{q=R(F,I,f?()=>d(f(),s):I)}catch(e){throw N.current&&(e.message+=`\nThe error may be correlated with this previous error:\n${N.current.stack}\n\n`),e}P((()=>{N.current=void 0,O.current=void 0,_.current=q}));const U=(0,a.useMemo)((()=>a.createElement(e,(0,g.Z)({},q,{ref:o}))),[o,e,q]);return(0,a.useMemo)((()=>v?a.createElement(i.Provider,{value:S},U):U),[i,U,S])}const d=a.memo(u);if(d.WrappedComponent=e,d.displayName=u.displayName=n,c){const t=a.forwardRef((function(e,t){return a.createElement(d,(0,g.Z)({},e,{reactReduxForwardedRef:t}))}));return t.displayName=n,t.WrappedComponent=e,b()(t,e)}return b()(d,e)}};const B=function({store:e,context:t,children:n,serverState:r}){const o=(0,a.useMemo)((()=>{const t=C(e);return{store:e,subscription:t,getServerState:r?()=>r:void 0}}),[e,r]),s=(0,a.useMemo)((()=>e.getState()),[e]);P((()=>{const{subscription:t}=o;return t.onStateChange=t.notifyNestedSubs,t.trySubscribe(),s!==e.getState()&&t.notifyNestedSubs(),()=>{t.tryUnsubscribe(),t.onStateChange=void 0}}),[o,s]);const i=t||d;return a.createElement(i.Provider,{value:o},n)};var $,q;$=p.useSyncExternalStoreWithSelector,m=$,(e=>{R=e})(u.useSyncExternalStore),q=l.unstable_batchedUpdates,h=q;var U=n(57557),z=n.n(U),V=n(6557),W=n.n(V);const J=e=>t=>{const{fn:n}=e();class r extends a.Component{render(){return a.createElement(t,o()({},e(),this.props,this.context))}}return r.displayName=`WithSystem(${n.getDisplayName(t)})`,r},K=(e,t)=>n=>{const{fn:r}=e();class s extends a.Component{render(){return a.createElement(B,{store:t},a.createElement(n,o()({},this.props,this.context)))}}return s.displayName=`WithRoot(${r.getDisplayName(n)})`,s},H=(e,t,n)=>(0,c.qC)(n?K(e,n):W(),L(((n,r)=>{var o;const s={...r,...e()},i=(null===(o=t.prototype)||void 0===o?void 0:o.mapStateToProps)||(e=>({state:e}));return i(n,s)})),J(e))(t),G=(e,t,n,r)=>{for(const o in t){const s=t[o];"function"==typeof s&&s(n[o],r[o],e())}},Z=(e,t,n)=>(t,r)=>{const{fn:o}=e(),s=n(t,"root");class l extends a.Component{constructor(t,n){super(t,n),G(e,r,t,{})}UNSAFE_componentWillReceiveProps(t){G(e,r,t,this.props)}render(){const e=z()(this.props,r?i()(r):[]);return a.createElement(s,e)}}return l.displayName=`WithMappedContainer(${o.getDisplayName(s)})`,l},Y=(e,t,n,r)=>o=>{const s=n(e,t,r)("App","root");l.render(a.createElement(s,null),o)},X=(e,t,n)=>function(r,o){let s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if("string"!=typeof r)throw new TypeError("Need a string, to fetch a component. Was given a "+typeof r);const i=n(r);return i?o?"root"===o?H(e,i,t()):H(e,i):i:(s.failSilently||e().log.warn("Could not find component:",r),null)}},33424:(e,t,n)=>{"use strict";n.d(t,{d3:()=>D,C2:()=>ee});var r=n(28222),o=n.n(r),s=n(58118),i=n.n(s),a=n(63366);function l(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n1&&void 0!==arguments[1]?arguments[1]:{},n=arguments.length>2?arguments[2]:void 0;return function(e){if(0===e.length||1===e.length)return e;var t,n,r=e.join(".");return m[r]||(m[r]=0===(n=(t=e).length)||1===n?t:2===n?[t[0],t[1],"".concat(t[0],".").concat(t[1]),"".concat(t[1],".").concat(t[0])]:3===n?[t[0],t[1],t[2],"".concat(t[0],".").concat(t[1]),"".concat(t[0],".").concat(t[2]),"".concat(t[1],".").concat(t[0]),"".concat(t[1],".").concat(t[2]),"".concat(t[2],".").concat(t[0]),"".concat(t[2],".").concat(t[1]),"".concat(t[0],".").concat(t[1],".").concat(t[2]),"".concat(t[0],".").concat(t[2],".").concat(t[1]),"".concat(t[1],".").concat(t[0],".").concat(t[2]),"".concat(t[1],".").concat(t[2],".").concat(t[0]),"".concat(t[2],".").concat(t[0],".").concat(t[1]),"".concat(t[2],".").concat(t[1],".").concat(t[0])]:n>=4?[t[0],t[1],t[2],t[3],"".concat(t[0],".").concat(t[1]),"".concat(t[0],".").concat(t[2]),"".concat(t[0],".").concat(t[3]),"".concat(t[1],".").concat(t[0]),"".concat(t[1],".").concat(t[2]),"".concat(t[1],".").concat(t[3]),"".concat(t[2],".").concat(t[0]),"".concat(t[2],".").concat(t[1]),"".concat(t[2],".").concat(t[3]),"".concat(t[3],".").concat(t[0]),"".concat(t[3],".").concat(t[1]),"".concat(t[3],".").concat(t[2]),"".concat(t[0],".").concat(t[1],".").concat(t[2]),"".concat(t[0],".").concat(t[1],".").concat(t[3]),"".concat(t[0],".").concat(t[2],".").concat(t[1]),"".concat(t[0],".").concat(t[2],".").concat(t[3]),"".concat(t[0],".").concat(t[3],".").concat(t[1]),"".concat(t[0],".").concat(t[3],".").concat(t[2]),"".concat(t[1],".").concat(t[0],".").concat(t[2]),"".concat(t[1],".").concat(t[0],".").concat(t[3]),"".concat(t[1],".").concat(t[2],".").concat(t[0]),"".concat(t[1],".").concat(t[2],".").concat(t[3]),"".concat(t[1],".").concat(t[3],".").concat(t[0]),"".concat(t[1],".").concat(t[3],".").concat(t[2]),"".concat(t[2],".").concat(t[0],".").concat(t[1]),"".concat(t[2],".").concat(t[0],".").concat(t[3]),"".concat(t[2],".").concat(t[1],".").concat(t[0]),"".concat(t[2],".").concat(t[1],".").concat(t[3]),"".concat(t[2],".").concat(t[3],".").concat(t[0]),"".concat(t[2],".").concat(t[3],".").concat(t[1]),"".concat(t[3],".").concat(t[0],".").concat(t[1]),"".concat(t[3],".").concat(t[0],".").concat(t[2]),"".concat(t[3],".").concat(t[1],".").concat(t[0]),"".concat(t[3],".").concat(t[1],".").concat(t[2]),"".concat(t[3],".").concat(t[2],".").concat(t[0]),"".concat(t[3],".").concat(t[2],".").concat(t[1]),"".concat(t[0],".").concat(t[1],".").concat(t[2],".").concat(t[3]),"".concat(t[0],".").concat(t[1],".").concat(t[3],".").concat(t[2]),"".concat(t[0],".").concat(t[2],".").concat(t[1],".").concat(t[3]),"".concat(t[0],".").concat(t[2],".").concat(t[3],".").concat(t[1]),"".concat(t[0],".").concat(t[3],".").concat(t[1],".").concat(t[2]),"".concat(t[0],".").concat(t[3],".").concat(t[2],".").concat(t[1]),"".concat(t[1],".").concat(t[0],".").concat(t[2],".").concat(t[3]),"".concat(t[1],".").concat(t[0],".").concat(t[3],".").concat(t[2]),"".concat(t[1],".").concat(t[2],".").concat(t[0],".").concat(t[3]),"".concat(t[1],".").concat(t[2],".").concat(t[3],".").concat(t[0]),"".concat(t[1],".").concat(t[3],".").concat(t[0],".").concat(t[2]),"".concat(t[1],".").concat(t[3],".").concat(t[2],".").concat(t[0]),"".concat(t[2],".").concat(t[0],".").concat(t[1],".").concat(t[3]),"".concat(t[2],".").concat(t[0],".").concat(t[3],".").concat(t[1]),"".concat(t[2],".").concat(t[1],".").concat(t[0],".").concat(t[3]),"".concat(t[2],".").concat(t[1],".").concat(t[3],".").concat(t[0]),"".concat(t[2],".").concat(t[3],".").concat(t[0],".").concat(t[1]),"".concat(t[2],".").concat(t[3],".").concat(t[1],".").concat(t[0]),"".concat(t[3],".").concat(t[0],".").concat(t[1],".").concat(t[2]),"".concat(t[3],".").concat(t[0],".").concat(t[2],".").concat(t[1]),"".concat(t[3],".").concat(t[1],".").concat(t[0],".").concat(t[2]),"".concat(t[3],".").concat(t[1],".").concat(t[2],".").concat(t[0]),"".concat(t[3],".").concat(t[2],".").concat(t[0],".").concat(t[1]),"".concat(t[3],".").concat(t[2],".").concat(t[1],".").concat(t[0])]:void 0),m[r]}(e.filter((function(e){return"token"!==e}))).reduce((function(e,t){return d(d({},e),n[t])}),t)}function y(e){return e.join(" ")}function v(e){var t=e.node,n=e.stylesheet,r=e.style,o=void 0===r?{}:r,s=e.useInlineStyles,i=e.key,a=t.properties,l=t.type,c=t.tagName,u=t.value;if("text"===l)return u;if(c){var f,m=function(e,t){var n=0;return function(r){return n+=1,r.map((function(r,o){return v({node:r,stylesheet:e,useInlineStyles:t,key:"code-segment-".concat(n,"-").concat(o)})}))}}(n,s);if(s){var b=Object.keys(n).reduce((function(e,t){return t.split(".").forEach((function(t){e.includes(t)||e.push(t)})),e}),[]),w=a.className&&a.className.includes("token")?["token"]:[],E=a.className&&w.concat(a.className.filter((function(e){return!b.includes(e)})));f=d(d({},a),{},{className:y(E)||void 0,style:g(a.className,Object.assign({},a.style,o),n)})}else f=d(d({},a),{},{className:y(a.className)});var x=m(t.children);return p.createElement(c,(0,h.Z)({key:i},f),x)}}const b=function(e,t){return-1!==e.listLanguages().indexOf(t)};var w=["language","children","style","customStyle","codeTagProps","useInlineStyles","showLineNumbers","showInlineLineNumbers","startingLineNumber","lineNumberContainerStyle","lineNumberStyle","wrapLines","wrapLongLines","lineProps","renderer","PreTag","CodeTag","code","astGenerator"];function E(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function x(e){for(var t=1;t1&&void 0!==arguments[1]?arguments[1]:[],n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[],r=0;r2&&void 0!==arguments[2]?arguments[2]:[];return t||c.length>0?function(e,t){return k({children:e,lineNumber:t,lineNumberStyle:a,largestLineNumber:i,showInlineLineNumbers:o,lineProps:n,className:arguments.length>2&&void 0!==arguments[2]?arguments[2]:[],showLineNumbers:r,wrapLongLines:l})}(e,s,c):function(e,t){if(r&&t&&o){var n=O(a,t,i);e.unshift(j(t,n))}return e}(e,s)}for(var m=function(){var e=u[f],t=e.children[0].value;if(t.match(S)){var n=t.split("\n");n.forEach((function(t,o){var i=r&&p.length+s,a={type:"text",value:"".concat(t,"\n")};if(0===o){var l=d(u.slice(h+1,f).concat(k({children:[a],className:e.properties.className})),i);p.push(l)}else if(o===n.length-1){var c=u[f+1]&&u[f+1].children&&u[f+1].children[0],m={type:"text",value:"".concat(t)};if(c){var g=k({children:[m],className:e.properties.className});u.splice(f+1,0,g)}else{var y=d([m],i,e.properties.className);p.push(y)}}else{var v=d([a],i,e.properties.className);p.push(v)}})),h=f}f++};f=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}(e,w);z=z||I;var W=d?p.createElement(_,{containerStyle:E,codeStyle:c.style||{},numberStyle:j,startingLineNumber:v,codeString:U}):null,J=o.hljs||o['pre[class*="language-"]']||{backgroundColor:"#fff"},K=N(z)?"hljs":"prismjs",H=h?Object.assign({},V,{style:Object.assign({},J,i)}):Object.assign({},V,{className:V.className?"".concat(K," ").concat(V.className):K,style:Object.assign({},i)});if(c.style=x(x({},c.style),{},A?{whiteSpace:"pre-wrap"}:{whiteSpace:"pre"}),!z)return p.createElement(L,H,W,p.createElement($,c,U));(void 0===O&&D||A)&&(O=!0),D=D||P;var G=[{type:"text",value:U}],Z=function(e){var t=e.astGenerator,n=e.language,r=e.code,o=e.defaultCodeValue;if(N(t)){var s=b(t,n);return"text"===n?{value:o,language:"text"}:s?t.highlight(n,r):t.highlightAuto(r)}try{return n&&"text"!==n?{value:t.highlight(r,n)}:{value:o}}catch(e){return{value:o}}}({astGenerator:z,language:t,code:U,defaultCodeValue:G});null===Z.language&&(Z.value=G);var Y=C(Z,O,M,d,g,v,Z.value.length+v,j,A);return p.createElement(L,H,p.createElement($,c,!g&&W,D({rows:Y,stylesheet:o,useInlineStyles:h})))});M.registerLanguage=R.registerLanguage;const D=M;var F=n(96344);const L=n.n(F)();var B=n(82026);const $=n.n(B)();var q=n(42157);const U=n.n(q)();var z=n(61519);const V=n.n(z)();var W=n(54587);const J=n.n(W)();var K=n(30786);const H=n.n(K)();var G=n(66336);const Z=n.n(G)(),Y={hljs:{display:"block",overflowX:"auto",padding:"0.5em",background:"#333",color:"white"},"hljs-name":{fontWeight:"bold"},"hljs-strong":{fontWeight:"bold"},"hljs-code":{fontStyle:"italic",color:"#888"},"hljs-emphasis":{fontStyle:"italic"},"hljs-tag":{color:"#62c8f3"},"hljs-variable":{color:"#ade5fc"},"hljs-template-variable":{color:"#ade5fc"},"hljs-selector-id":{color:"#ade5fc"},"hljs-selector-class":{color:"#ade5fc"},"hljs-string":{color:"#a2fca2"},"hljs-bullet":{color:"#d36363"},"hljs-type":{color:"#ffa"},"hljs-title":{color:"#ffa"},"hljs-section":{color:"#ffa"},"hljs-attribute":{color:"#ffa"},"hljs-quote":{color:"#ffa"},"hljs-built_in":{color:"#ffa"},"hljs-builtin-name":{color:"#ffa"},"hljs-number":{color:"#d36363"},"hljs-symbol":{color:"#d36363"},"hljs-keyword":{color:"#fcc28c"},"hljs-selector-tag":{color:"#fcc28c"},"hljs-literal":{color:"#fcc28c"},"hljs-comment":{color:"#888"},"hljs-deletion":{color:"#333",backgroundColor:"#fc9b9b"},"hljs-regexp":{color:"#c6b4f0"},"hljs-link":{color:"#c6b4f0"},"hljs-meta":{color:"#fc9b9b"},"hljs-addition":{backgroundColor:"#a2fca2",color:"#333"}};D.registerLanguage("json",$),D.registerLanguage("js",L),D.registerLanguage("xml",U),D.registerLanguage("yaml",J),D.registerLanguage("http",H),D.registerLanguage("bash",V),D.registerLanguage("powershell",Z),D.registerLanguage("javascript",L);const X={agate:Y,arta:{hljs:{display:"block",overflowX:"auto",padding:"0.5em",background:"#222",color:"#aaa"},"hljs-subst":{color:"#aaa"},"hljs-section":{color:"#fff",fontWeight:"bold"},"hljs-comment":{color:"#444"},"hljs-quote":{color:"#444"},"hljs-meta":{color:"#444"},"hljs-string":{color:"#ffcc33"},"hljs-symbol":{color:"#ffcc33"},"hljs-bullet":{color:"#ffcc33"},"hljs-regexp":{color:"#ffcc33"},"hljs-number":{color:"#00cc66"},"hljs-addition":{color:"#00cc66"},"hljs-built_in":{color:"#32aaee"},"hljs-builtin-name":{color:"#32aaee"},"hljs-literal":{color:"#32aaee"},"hljs-type":{color:"#32aaee"},"hljs-template-variable":{color:"#32aaee"},"hljs-attribute":{color:"#32aaee"},"hljs-link":{color:"#32aaee"},"hljs-keyword":{color:"#6644aa"},"hljs-selector-tag":{color:"#6644aa"},"hljs-name":{color:"#6644aa"},"hljs-selector-id":{color:"#6644aa"},"hljs-selector-class":{color:"#6644aa"},"hljs-title":{color:"#bb1166"},"hljs-variable":{color:"#bb1166"},"hljs-deletion":{color:"#bb1166"},"hljs-template-tag":{color:"#bb1166"},"hljs-doctag":{fontWeight:"bold"},"hljs-strong":{fontWeight:"bold"},"hljs-emphasis":{fontStyle:"italic"}},monokai:{hljs:{display:"block",overflowX:"auto",padding:"0.5em",background:"#272822",color:"#ddd"},"hljs-tag":{color:"#f92672"},"hljs-keyword":{color:"#f92672",fontWeight:"bold"},"hljs-selector-tag":{color:"#f92672",fontWeight:"bold"},"hljs-literal":{color:"#f92672",fontWeight:"bold"},"hljs-strong":{color:"#f92672"},"hljs-name":{color:"#f92672"},"hljs-code":{color:"#66d9ef"},"hljs-class .hljs-title":{color:"white"},"hljs-attribute":{color:"#bf79db"},"hljs-symbol":{color:"#bf79db"},"hljs-regexp":{color:"#bf79db"},"hljs-link":{color:"#bf79db"},"hljs-string":{color:"#a6e22e"},"hljs-bullet":{color:"#a6e22e"},"hljs-subst":{color:"#a6e22e"},"hljs-title":{color:"#a6e22e",fontWeight:"bold"},"hljs-section":{color:"#a6e22e",fontWeight:"bold"},"hljs-emphasis":{color:"#a6e22e"},"hljs-type":{color:"#a6e22e",fontWeight:"bold"},"hljs-built_in":{color:"#a6e22e"},"hljs-builtin-name":{color:"#a6e22e"},"hljs-selector-attr":{color:"#a6e22e"},"hljs-selector-pseudo":{color:"#a6e22e"},"hljs-addition":{color:"#a6e22e"},"hljs-variable":{color:"#a6e22e"},"hljs-template-tag":{color:"#a6e22e"},"hljs-template-variable":{color:"#a6e22e"},"hljs-comment":{color:"#75715e"},"hljs-quote":{color:"#75715e"},"hljs-deletion":{color:"#75715e"},"hljs-meta":{color:"#75715e"},"hljs-doctag":{fontWeight:"bold"},"hljs-selector-id":{fontWeight:"bold"}},nord:{hljs:{display:"block",overflowX:"auto",padding:"0.5em",background:"#2E3440",color:"#D8DEE9"},"hljs-subst":{color:"#D8DEE9"},"hljs-selector-tag":{color:"#81A1C1"},"hljs-selector-id":{color:"#8FBCBB",fontWeight:"bold"},"hljs-selector-class":{color:"#8FBCBB"},"hljs-selector-attr":{color:"#8FBCBB"},"hljs-selector-pseudo":{color:"#88C0D0"},"hljs-addition":{backgroundColor:"rgba(163, 190, 140, 0.5)"},"hljs-deletion":{backgroundColor:"rgba(191, 97, 106, 0.5)"},"hljs-built_in":{color:"#8FBCBB"},"hljs-type":{color:"#8FBCBB"},"hljs-class":{color:"#8FBCBB"},"hljs-function":{color:"#88C0D0"},"hljs-function > .hljs-title":{color:"#88C0D0"},"hljs-keyword":{color:"#81A1C1"},"hljs-literal":{color:"#81A1C1"},"hljs-symbol":{color:"#81A1C1"},"hljs-number":{color:"#B48EAD"},"hljs-regexp":{color:"#EBCB8B"},"hljs-string":{color:"#A3BE8C"},"hljs-title":{color:"#8FBCBB"},"hljs-params":{color:"#D8DEE9"},"hljs-bullet":{color:"#81A1C1"},"hljs-code":{color:"#8FBCBB"},"hljs-emphasis":{fontStyle:"italic"},"hljs-formula":{color:"#8FBCBB"},"hljs-strong":{fontWeight:"bold"},"hljs-link:hover":{textDecoration:"underline"},"hljs-quote":{color:"#4C566A"},"hljs-comment":{color:"#4C566A"},"hljs-doctag":{color:"#8FBCBB"},"hljs-meta":{color:"#5E81AC"},"hljs-meta-keyword":{color:"#5E81AC"},"hljs-meta-string":{color:"#A3BE8C"},"hljs-attr":{color:"#8FBCBB"},"hljs-attribute":{color:"#D8DEE9"},"hljs-builtin-name":{color:"#81A1C1"},"hljs-name":{color:"#81A1C1"},"hljs-section":{color:"#88C0D0"},"hljs-tag":{color:"#81A1C1"},"hljs-variable":{color:"#D8DEE9"},"hljs-template-variable":{color:"#D8DEE9"},"hljs-template-tag":{color:"#5E81AC"},"abnf .hljs-attribute":{color:"#88C0D0"},"abnf .hljs-symbol":{color:"#EBCB8B"},"apache .hljs-attribute":{color:"#88C0D0"},"apache .hljs-section":{color:"#81A1C1"},"arduino .hljs-built_in":{color:"#88C0D0"},"aspectj .hljs-meta":{color:"#D08770"},"aspectj > .hljs-title":{color:"#88C0D0"},"bnf .hljs-attribute":{color:"#8FBCBB"},"clojure .hljs-name":{color:"#88C0D0"},"clojure .hljs-symbol":{color:"#EBCB8B"},"coq .hljs-built_in":{color:"#88C0D0"},"cpp .hljs-meta-string":{color:"#8FBCBB"},"css .hljs-built_in":{color:"#88C0D0"},"css .hljs-keyword":{color:"#D08770"},"diff .hljs-meta":{color:"#8FBCBB"},"ebnf .hljs-attribute":{color:"#8FBCBB"},"glsl .hljs-built_in":{color:"#88C0D0"},"groovy .hljs-meta:not(:first-child)":{color:"#D08770"},"haxe .hljs-meta":{color:"#D08770"},"java .hljs-meta":{color:"#D08770"},"ldif .hljs-attribute":{color:"#8FBCBB"},"lisp .hljs-name":{color:"#88C0D0"},"lua .hljs-built_in":{color:"#88C0D0"},"moonscript .hljs-built_in":{color:"#88C0D0"},"nginx .hljs-attribute":{color:"#88C0D0"},"nginx .hljs-section":{color:"#5E81AC"},"pf .hljs-built_in":{color:"#88C0D0"},"processing .hljs-built_in":{color:"#88C0D0"},"scss .hljs-keyword":{color:"#81A1C1"},"stylus .hljs-keyword":{color:"#81A1C1"},"swift .hljs-meta":{color:"#D08770"},"vim .hljs-built_in":{color:"#88C0D0",fontStyle:"italic"},"yaml .hljs-meta":{color:"#D08770"}},obsidian:{hljs:{display:"block",overflowX:"auto",padding:"0.5em",background:"#282b2e",color:"#e0e2e4"},"hljs-keyword":{color:"#93c763",fontWeight:"bold"},"hljs-selector-tag":{color:"#93c763",fontWeight:"bold"},"hljs-literal":{color:"#93c763",fontWeight:"bold"},"hljs-selector-id":{color:"#93c763"},"hljs-number":{color:"#ffcd22"},"hljs-attribute":{color:"#668bb0"},"hljs-code":{color:"white"},"hljs-class .hljs-title":{color:"white"},"hljs-section":{color:"white",fontWeight:"bold"},"hljs-regexp":{color:"#d39745"},"hljs-link":{color:"#d39745"},"hljs-meta":{color:"#557182"},"hljs-tag":{color:"#8cbbad"},"hljs-name":{color:"#8cbbad",fontWeight:"bold"},"hljs-bullet":{color:"#8cbbad"},"hljs-subst":{color:"#8cbbad"},"hljs-emphasis":{color:"#8cbbad"},"hljs-type":{color:"#8cbbad",fontWeight:"bold"},"hljs-built_in":{color:"#8cbbad"},"hljs-selector-attr":{color:"#8cbbad"},"hljs-selector-pseudo":{color:"#8cbbad"},"hljs-addition":{color:"#8cbbad"},"hljs-variable":{color:"#8cbbad"},"hljs-template-tag":{color:"#8cbbad"},"hljs-template-variable":{color:"#8cbbad"},"hljs-string":{color:"#ec7600"},"hljs-symbol":{color:"#ec7600"},"hljs-comment":{color:"#818e96"},"hljs-quote":{color:"#818e96"},"hljs-deletion":{color:"#818e96"},"hljs-selector-class":{color:"#A082BD"},"hljs-doctag":{fontWeight:"bold"},"hljs-title":{fontWeight:"bold"},"hljs-strong":{fontWeight:"bold"}},"tomorrow-night":{"hljs-comment":{color:"#969896"},"hljs-quote":{color:"#969896"},"hljs-variable":{color:"#cc6666"},"hljs-template-variable":{color:"#cc6666"},"hljs-tag":{color:"#cc6666"},"hljs-name":{color:"#cc6666"},"hljs-selector-id":{color:"#cc6666"},"hljs-selector-class":{color:"#cc6666"},"hljs-regexp":{color:"#cc6666"},"hljs-deletion":{color:"#cc6666"},"hljs-number":{color:"#de935f"},"hljs-built_in":{color:"#de935f"},"hljs-builtin-name":{color:"#de935f"},"hljs-literal":{color:"#de935f"},"hljs-type":{color:"#de935f"},"hljs-params":{color:"#de935f"},"hljs-meta":{color:"#de935f"},"hljs-link":{color:"#de935f"},"hljs-attribute":{color:"#f0c674"},"hljs-string":{color:"#b5bd68"},"hljs-symbol":{color:"#b5bd68"},"hljs-bullet":{color:"#b5bd68"},"hljs-addition":{color:"#b5bd68"},"hljs-title":{color:"#81a2be"},"hljs-section":{color:"#81a2be"},"hljs-keyword":{color:"#b294bb"},"hljs-selector-tag":{color:"#b294bb"},hljs:{display:"block",overflowX:"auto",background:"#1d1f21",color:"#c5c8c6",padding:"0.5em"},"hljs-emphasis":{fontStyle:"italic"},"hljs-strong":{fontWeight:"bold"}}},Q=o()(X),ee=e=>i()(Q).call(Q,e)?X[e]:(console.warn(`Request style '${e}' is not available, returning default instead`),Y)},90242:(e,t,n)=>{"use strict";n.d(t,{AF:()=>ae,Ay:()=>fe,D$:()=>De,DR:()=>ve,GZ:()=>je,HP:()=>he,Ik:()=>Ee,J6:()=>Ne,Kn:()=>ce,LQ:()=>le,Nm:()=>ke,O2:()=>Ue,Pz:()=>Me,Q2:()=>de,QG:()=>Ce,UG:()=>xe,Uj:()=>Be,V9:()=>Fe,Wl:()=>ue,XV:()=>Re,Xb:()=>$e,Zl:()=>be,_5:()=>me,be:()=>Oe,cz:()=>Le,gp:()=>ye,hW:()=>Ae,iQ:()=>ge,kJ:()=>pe,mz:()=>se,nX:()=>Ie,oG:()=>ie,oJ:()=>Pe,po:()=>Te,r3:()=>Se,wh:()=>_e});var r=n(58309),o=n.n(r),s=n(97606),i=n.n(s),a=n(74386),l=n.n(a),c=n(86),u=n.n(c),p=n(14418),h=n.n(p),f=n(28222),d=n.n(f),m=(n(11189),n(24282)),g=n.n(m),y=n(76986),v=n.n(y),b=n(2578),w=n.n(b),E=(n(24278),n(39022),n(92039)),x=n.n(E),S=(n(58118),n(11882)),_=n.n(S),j=n(51679),O=n.n(j),k=n(27043),A=n.n(k),C=n(81607),P=n.n(C),N=n(35627),I=n.n(N),T=n(43393),R=n.n(T),M=n(17967),D=n(68929),F=n.n(D),L=n(11700),B=n.n(L),$=n(88306),q=n.n($),U=n(13311),z=n.n(U),V=(n(59704),n(77813)),W=n.n(V),J=n(23560),K=n.n(J),H=n(27504),G=n(8269),Z=n.n(G),Y=n(19069),X=n(92282),Q=n.n(X),ee=n(89072),te=n.n(ee),ne=n(48764).Buffer;const re="default",oe=e=>R().Iterable.isIterable(e);function se(e){return ce(e)?oe(e)?e.toJS():e:{}}function ie(e){var t,n;if(oe(e))return e;if(e instanceof H.Z.File)return e;if(!ce(e))return e;if(o()(e))return i()(n=R().Seq(e)).call(n,ie).toList();if(K()(l()(e))){var r;const t=function(e){if(!K()(l()(e)))return e;const t={},n="_**[]",r={};for(let o of l()(e).call(e))if(t[o[0]]||r[o[0]]&&r[o[0]].containsMultiple){if(!r[o[0]]){r[o[0]]={containsMultiple:!0,length:1},t[`${o[0]}${n}${r[o[0]].length}`]=t[o[0]],delete t[o[0]]}r[o[0]].length+=1,t[`${o[0]}${n}${r[o[0]].length}`]=o[1]}else t[o[0]]=o[1];return t}(e);return i()(r=R().OrderedMap(t)).call(r,ie)}return i()(t=R().OrderedMap(e)).call(t,ie)}function ae(e){return o()(e)?e:[e]}function le(e){return"function"==typeof e}function ce(e){return!!e&&"object"==typeof e}function ue(e){return"function"==typeof e}function pe(e){return o()(e)}const he=q();function fe(e,t){var n;return g()(n=d()(e)).call(n,((n,r)=>(n[r]=t(e[r],r),n)),{})}function de(e,t){var n;return g()(n=d()(e)).call(n,((n,r)=>{let o=t(e[r],r);return o&&"object"==typeof o&&v()(n,o),n}),{})}function me(e){return t=>{let{dispatch:n,getState:r}=t;return t=>n=>"function"==typeof n?n(e()):t(n)}}function ge(e){var t;let n=e.keySeq();return n.contains(re)?re:w()(t=h()(n).call(n,(e=>"2"===(e+"")[0]))).call(t).first()}function ye(e,t){if(!R().Iterable.isIterable(e))return R().List();let n=e.getIn(o()(t)?t:[t]);return R().List.isList(n)?n:R().List()}function ve(e){let t,n=[/filename\*=[^']+'\w*'"([^"]+)";?/i,/filename\*=[^']+'\w*'([^;]+);?/i,/filename="([^;]*);?"/i,/filename=([^;]*);?/i];if(x()(n).call(n,(n=>(t=n.exec(e),null!==t))),null!==t&&t.length>1)try{return decodeURIComponent(t[1])}catch(e){console.error(e)}return null}function be(e){return t=e.replace(/\.[^./]*$/,""),B()(F()(t));var t}function we(e,t,n,r,s){if(!t)return[];let a=[],l=t.get("nullable"),c=t.get("required"),p=t.get("maximum"),f=t.get("minimum"),d=t.get("type"),m=t.get("format"),g=t.get("maxLength"),y=t.get("minLength"),v=t.get("uniqueItems"),b=t.get("maxItems"),w=t.get("minItems"),E=t.get("pattern");const S=n||!0===c,_=null!=e;if(l&&null===e||!d||!(S||_&&"array"===d||!(!S&&!_)))return[];let j="string"===d&&e,O="array"===d&&o()(e)&&e.length,k="array"===d&&R().List.isList(e)&&e.count();const A=[j,O,k,"array"===d&&"string"==typeof e&&e,"file"===d&&e instanceof H.Z.File,"boolean"===d&&(e||!1===e),"number"===d&&(e||0===e),"integer"===d&&(e||0===e),"object"===d&&"object"==typeof e&&null!==e,"object"===d&&"string"==typeof e&&e],C=x()(A).call(A,(e=>!!e));if(S&&!C&&!r)return a.push("Required field is not provided"),a;if("object"===d&&(null===s||"application/json"===s)){let n=e;if("string"==typeof e)try{n=JSON.parse(e)}catch(e){return a.push("Parameter string value must be valid JSON"),a}var P;if(t&&t.has("required")&&ue(c.isList)&&c.isList()&&u()(c).call(c,(e=>{void 0===n[e]&&a.push({propKey:e,error:"Required property not found"})})),t&&t.has("properties"))u()(P=t.get("properties")).call(P,((e,t)=>{const o=we(n[t],e,!1,r,s);a.push(...i()(o).call(o,(e=>({propKey:t,error:e}))))}))}if(E){let t=((e,t)=>{if(!new RegExp(t).test(e))return"Value must follow pattern "+t})(e,E);t&&a.push(t)}if(w&&"array"===d){let t=((e,t)=>{if(!e&&t>=1||e&&e.length{if(e&&e.length>t)return`Array must not contain more then ${t} item${1===t?"":"s"}`})(e,b);t&&a.push({needRemove:!0,error:t})}if(v&&"array"===d){let t=((e,t)=>{if(e&&("true"===t||!0===t)){const t=(0,T.fromJS)(e),n=t.toSet();if(e.length>n.size){let e=(0,T.Set)();if(u()(t).call(t,((n,r)=>{h()(t).call(t,(e=>ue(e.equals)?e.equals(n):e===n)).size>1&&(e=e.add(r))})),0!==e.size)return i()(e).call(e,(e=>({index:e,error:"No duplicates allowed."}))).toArray()}}})(e,v);t&&a.push(...t)}if(g||0===g){let t=((e,t)=>{if(e.length>t)return`Value must be no longer than ${t} character${1!==t?"s":""}`})(e,g);t&&a.push(t)}if(y){let t=((e,t)=>{if(e.length{if(e>t)return`Value must be less than ${t}`})(e,p);t&&a.push(t)}if(f||0===f){let t=((e,t)=>{if(e{if(isNaN(Date.parse(e)))return"Value must be a DateTime"})(e):"uuid"===m?(e=>{if(e=e.toString().toLowerCase(),!/^[{(]?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}[)}]?$/.test(e))return"Value must be a Guid"})(e):(e=>{if(e&&"string"!=typeof e)return"Value must be a string"})(e),!t)return a;a.push(t)}else if("boolean"===d){let t=(e=>{if("true"!==e&&"false"!==e&&!0!==e&&!1!==e)return"Value must be a boolean"})(e);if(!t)return a;a.push(t)}else if("number"===d){let t=(e=>{if(!/^-?\d+(\.?\d+)?$/.test(e))return"Value must be a number"})(e);if(!t)return a;a.push(t)}else if("integer"===d){let t=(e=>{if(!/^-?\d+$/.test(e))return"Value must be an integer"})(e);if(!t)return a;a.push(t)}else if("array"===d){if(!O&&!k)return a;e&&u()(e).call(e,((e,n)=>{const o=we(e,t.get("items"),!1,r,s);a.push(...i()(o).call(o,(e=>({index:n,error:e}))))}))}else if("file"===d){let t=(e=>{if(e&&!(e instanceof H.Z.File))return"Value must be a file"})(e);if(!t)return a;a.push(t)}return a}const Ee=function(e,t){let{isOAS3:n=!1,bypassRequiredCheck:r=!1}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},o=e.get("required"),{schema:s,parameterContentMediaType:i}=(0,Y.Z)(e,{isOAS3:n});return we(t,s,o,r,i)},xe=()=>{let e={},t=H.Z.location.search;if(!t)return{};if(""!=t){let n=t.substr(1).split("&");for(let t in n)Object.prototype.hasOwnProperty.call(n,t)&&(t=n[t].split("="),e[decodeURIComponent(t[0])]=t[1]&&decodeURIComponent(t[1])||"")}return e},Se=e=>{let t;return t=e instanceof ne?e:ne.from(e.toString(),"utf-8"),t.toString("base64")},_e={operationsSorter:{alpha:(e,t)=>e.get("path").localeCompare(t.get("path")),method:(e,t)=>e.get("method").localeCompare(t.get("method"))},tagsSorter:{alpha:(e,t)=>e.localeCompare(t)}},je=e=>{let t=[];for(let n in e){let r=e[n];void 0!==r&&""!==r&&t.push([n,"=",encodeURIComponent(r).replace(/%20/g,"+")].join(""))}return t.join("&")},Oe=(e,t,n)=>!!z()(n,(n=>W()(e[n],t[n])));function ke(e){return"string"!=typeof e||""===e?"":(0,M.N)(e)}function Ae(e){return!(!e||_()(e).call(e,"localhost")>=0||_()(e).call(e,"127.0.0.1")>=0||"none"===e)}function Ce(e){if(!R().OrderedMap.isOrderedMap(e))return null;if(!e.size)return null;const t=O()(e).call(e,((e,t)=>A()(t).call(t,"2")&&d()(e.get("content")||{}).length>0)),n=e.get("default")||R().OrderedMap(),r=(n.get("content")||R().OrderedMap()).keySeq().toJS().length?n:null;return t||r}const Pe=e=>"string"==typeof e||e instanceof String?P()(e).call(e).replace(/\s/g,"%20"):"",Ne=e=>Z()(Pe(e).replace(/%20/g,"_")),Ie=e=>h()(e).call(e,((e,t)=>/^x-/.test(t))),Te=e=>h()(e).call(e,((e,t)=>/^pattern|maxLength|minLength|maximum|minimum/.test(t)));function Re(e,t){var n;let r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:()=>!0;if("object"!=typeof e||o()(e)||null===e||!t)return e;const s=v()({},e);return u()(n=d()(s)).call(n,(e=>{e===t&&r(s[e],e)?delete s[e]:s[e]=Re(s[e],t,r)})),s}function Me(e){if("string"==typeof e)return e;if(e&&e.toJS&&(e=e.toJS()),"object"==typeof e&&null!==e)try{return I()(e,null,2)}catch(t){return String(e)}return null==e?"":e.toString()}function De(e){return"number"==typeof e?e.toString():e}function Fe(e){let{returnAll:t=!1,allowHashes:n=!0}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(!R().Map.isMap(e))throw new Error("paramToIdentifier: received a non-Im.Map parameter as input");const r=e.get("name"),o=e.get("in");let s=[];return e&&e.hashCode&&o&&r&&n&&s.push(`${o}.${r}.hash-${e.hashCode()}`),o&&r&&s.push(`${o}.${r}`),s.push(r),t?s:s[0]||""}function Le(e,t){var n;const r=Fe(e,{returnAll:!0});return h()(n=i()(r).call(r,(e=>t[e]))).call(n,(e=>void 0!==e))[0]}function Be(){return qe(Q()(32).toString("base64"))}function $e(e){return qe(te()("sha256").update(e).digest("base64"))}function qe(e){return e.replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}const Ue=e=>!e||!(!oe(e)||!e.isEmpty())},2518:(e,t,n)=>{"use strict";function r(e){return function(e){try{return!!JSON.parse(e)}catch(e){return null}}(e)?"json":null}n.d(t,{O:()=>r})},63543:(e,t,n)=>{"use strict";n.d(t,{mn:()=>a});var r=n(63460),o=n.n(r);function s(e){return e.match(/^(?:[a-z]+:)?\/\//i)}function i(e,t){return e?s(e)?function(e){return e.match(/^\/\//i)?`${window.location.protocol}${e}`:e}(e):new(o())(e,t).href:t}function a(e,t){let{selectedServer:n=""}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};try{return function(e,t){let{selectedServer:n=""}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if(!e)return;if(s(e))return e;const r=i(n,t);return s(r)?new(o())(e,r).href:new(o())(e,window.location.href).href}(e,t,{selectedServer:n})}catch{return}}},27504:(e,t,n)=>{"use strict";n.d(t,{Z:()=>r});const r=function(){var e={location:{},history:{},open:()=>{},close:()=>{},File:function(){}};if("undefined"==typeof window)return e;try{e=window;for(var t of["File","Blob","FormData"])t in window&&(e[t]=window[t])}catch(e){console.error(e)}return e}()},19069:(e,t,n)=>{"use strict";n.d(t,{Z:()=>u});var r=n(14418),o=n.n(r),s=n(58118),i=n.n(s),a=n(43393),l=n.n(a);const c=l().Set.of("type","format","items","default","maximum","exclusiveMaximum","minimum","exclusiveMinimum","maxLength","minLength","pattern","maxItems","minItems","uniqueItems","enum","multipleOf");function u(e){let{isOAS3:t}=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(!l().Map.isMap(e))return{schema:l().Map(),parameterContentMediaType:null};if(!t)return"body"===e.get("in")?{schema:e.get("schema",l().Map()),parameterContentMediaType:null}:{schema:o()(e).call(e,((e,t)=>i()(c).call(c,t))),parameterContentMediaType:null};if(e.get("content")){const t=e.get("content",l().Map({})).keySeq().first();return{schema:e.getIn(["content",t,"schema"],l().Map()),parameterContentMediaType:t}}return{schema:e.get("schema")?e.get("schema",l().Map()):l().Map(),parameterContentMediaType:null}}},60314:(e,t,n)=>{"use strict";n.d(t,{Z:()=>x});var r=n(58309),o=n.n(r),s=n(2250),i=n.n(s),a=n(25110),l=n.n(a),c=n(8712),u=n.n(c),p=n(51679),h=n.n(p),f=n(12373),d=n.n(f),m=n(18492),g=n.n(m),y=n(88306),v=n.n(y);const b=e=>t=>o()(e)&&o()(t)&&e.length===t.length&&i()(e).call(e,((e,n)=>e===t[n])),w=function(){for(var e=arguments.length,t=new Array(e),n=0;n1&&void 0!==arguments[1]?arguments[1]:w;const{Cache:n}=v();v().Cache=E;const r=v()(e,t);return v().Cache=n,r}},79742:(e,t)=>{"use strict";t.byteLength=function(e){var t=a(e),n=t[0],r=t[1];return 3*(n+r)/4-r},t.toByteArray=function(e){var t,n,s=a(e),i=s[0],l=s[1],c=new o(function(e,t,n){return 3*(t+n)/4-n}(0,i,l)),u=0,p=l>0?i-4:i;for(n=0;n>16&255,c[u++]=t>>8&255,c[u++]=255&t;2===l&&(t=r[e.charCodeAt(n)]<<2|r[e.charCodeAt(n+1)]>>4,c[u++]=255&t);1===l&&(t=r[e.charCodeAt(n)]<<10|r[e.charCodeAt(n+1)]<<4|r[e.charCodeAt(n+2)]>>2,c[u++]=t>>8&255,c[u++]=255&t);return c},t.fromByteArray=function(e){for(var t,r=e.length,o=r%3,s=[],i=16383,a=0,c=r-o;ac?c:a+i));1===o?(t=e[r-1],s.push(n[t>>2]+n[t<<4&63]+"==")):2===o&&(t=(e[r-2]<<8)+e[r-1],s.push(n[t>>10]+n[t>>4&63]+n[t<<2&63]+"="));return s.join("")};for(var n=[],r=[],o="undefined"!=typeof Uint8Array?Uint8Array:Array,s="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",i=0;i<64;++i)n[i]=s[i],r[s.charCodeAt(i)]=i;function a(e){var t=e.length;if(t%4>0)throw new Error("Invalid string. Length must be a multiple of 4");var n=e.indexOf("=");return-1===n&&(n=t),[n,n===t?0:4-n%4]}function l(e,t,r){for(var o,s,i=[],a=t;a>18&63]+n[s>>12&63]+n[s>>6&63]+n[63&s]);return i.join("")}r["-".charCodeAt(0)]=62,r["_".charCodeAt(0)]=63},48764:(e,t,n)=>{"use strict";const r=n(79742),o=n(80645),s="function"==typeof Symbol&&"function"==typeof Symbol.for?Symbol.for("nodejs.util.inspect.custom"):null;t.Buffer=l,t.SlowBuffer=function(e){+e!=e&&(e=0);return l.alloc(+e)},t.INSPECT_MAX_BYTES=50;const i=2147483647;function a(e){if(e>i)throw new RangeError('The value "'+e+'" is invalid for option "size"');const t=new Uint8Array(e);return Object.setPrototypeOf(t,l.prototype),t}function l(e,t,n){if("number"==typeof e){if("string"==typeof t)throw new TypeError('The "string" argument must be of type string. Received type number');return p(e)}return c(e,t,n)}function c(e,t,n){if("string"==typeof e)return function(e,t){"string"==typeof t&&""!==t||(t="utf8");if(!l.isEncoding(t))throw new TypeError("Unknown encoding: "+t);const n=0|m(e,t);let r=a(n);const o=r.write(e,t);o!==n&&(r=r.slice(0,o));return r}(e,t);if(ArrayBuffer.isView(e))return function(e){if(G(e,Uint8Array)){const t=new Uint8Array(e);return f(t.buffer,t.byteOffset,t.byteLength)}return h(e)}(e);if(null==e)throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof e);if(G(e,ArrayBuffer)||e&&G(e.buffer,ArrayBuffer))return f(e,t,n);if("undefined"!=typeof SharedArrayBuffer&&(G(e,SharedArrayBuffer)||e&&G(e.buffer,SharedArrayBuffer)))return f(e,t,n);if("number"==typeof e)throw new TypeError('The "value" argument must not be of type number. Received type number');const r=e.valueOf&&e.valueOf();if(null!=r&&r!==e)return l.from(r,t,n);const o=function(e){if(l.isBuffer(e)){const t=0|d(e.length),n=a(t);return 0===n.length||e.copy(n,0,0,t),n}if(void 0!==e.length)return"number"!=typeof e.length||Z(e.length)?a(0):h(e);if("Buffer"===e.type&&Array.isArray(e.data))return h(e.data)}(e);if(o)return o;if("undefined"!=typeof Symbol&&null!=Symbol.toPrimitive&&"function"==typeof e[Symbol.toPrimitive])return l.from(e[Symbol.toPrimitive]("string"),t,n);throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof e)}function u(e){if("number"!=typeof e)throw new TypeError('"size" argument must be of type number');if(e<0)throw new RangeError('The value "'+e+'" is invalid for option "size"')}function p(e){return u(e),a(e<0?0:0|d(e))}function h(e){const t=e.length<0?0:0|d(e.length),n=a(t);for(let r=0;r=i)throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+i.toString(16)+" bytes");return 0|e}function m(e,t){if(l.isBuffer(e))return e.length;if(ArrayBuffer.isView(e)||G(e,ArrayBuffer))return e.byteLength;if("string"!=typeof e)throw new TypeError('The "string" argument must be one of type string, Buffer, or ArrayBuffer. Received type '+typeof e);const n=e.length,r=arguments.length>2&&!0===arguments[2];if(!r&&0===n)return 0;let o=!1;for(;;)switch(t){case"ascii":case"latin1":case"binary":return n;case"utf8":case"utf-8":return J(e).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return K(e).length;default:if(o)return r?-1:J(e).length;t=(""+t).toLowerCase(),o=!0}}function g(e,t,n){let r=!1;if((void 0===t||t<0)&&(t=0),t>this.length)return"";if((void 0===n||n>this.length)&&(n=this.length),n<=0)return"";if((n>>>=0)<=(t>>>=0))return"";for(e||(e="utf8");;)switch(e){case"hex":return P(this,t,n);case"utf8":case"utf-8":return O(this,t,n);case"ascii":return A(this,t,n);case"latin1":case"binary":return C(this,t,n);case"base64":return j(this,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return N(this,t,n);default:if(r)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),r=!0}}function y(e,t,n){const r=e[t];e[t]=e[n],e[n]=r}function v(e,t,n,r,o){if(0===e.length)return-1;if("string"==typeof n?(r=n,n=0):n>2147483647?n=2147483647:n<-2147483648&&(n=-2147483648),Z(n=+n)&&(n=o?0:e.length-1),n<0&&(n=e.length+n),n>=e.length){if(o)return-1;n=e.length-1}else if(n<0){if(!o)return-1;n=0}if("string"==typeof t&&(t=l.from(t,r)),l.isBuffer(t))return 0===t.length?-1:b(e,t,n,r,o);if("number"==typeof t)return t&=255,"function"==typeof Uint8Array.prototype.indexOf?o?Uint8Array.prototype.indexOf.call(e,t,n):Uint8Array.prototype.lastIndexOf.call(e,t,n):b(e,[t],n,r,o);throw new TypeError("val must be string, number or Buffer")}function b(e,t,n,r,o){let s,i=1,a=e.length,l=t.length;if(void 0!==r&&("ucs2"===(r=String(r).toLowerCase())||"ucs-2"===r||"utf16le"===r||"utf-16le"===r)){if(e.length<2||t.length<2)return-1;i=2,a/=2,l/=2,n/=2}function c(e,t){return 1===i?e[t]:e.readUInt16BE(t*i)}if(o){let r=-1;for(s=n;sa&&(n=a-l),s=n;s>=0;s--){let n=!0;for(let r=0;ro&&(r=o):r=o;const s=t.length;let i;for(r>s/2&&(r=s/2),i=0;i>8,o=n%256,s.push(o),s.push(r);return s}(t,e.length-n),e,n,r)}function j(e,t,n){return 0===t&&n===e.length?r.fromByteArray(e):r.fromByteArray(e.slice(t,n))}function O(e,t,n){n=Math.min(e.length,n);const r=[];let o=t;for(;o239?4:t>223?3:t>191?2:1;if(o+i<=n){let n,r,a,l;switch(i){case 1:t<128&&(s=t);break;case 2:n=e[o+1],128==(192&n)&&(l=(31&t)<<6|63&n,l>127&&(s=l));break;case 3:n=e[o+1],r=e[o+2],128==(192&n)&&128==(192&r)&&(l=(15&t)<<12|(63&n)<<6|63&r,l>2047&&(l<55296||l>57343)&&(s=l));break;case 4:n=e[o+1],r=e[o+2],a=e[o+3],128==(192&n)&&128==(192&r)&&128==(192&a)&&(l=(15&t)<<18|(63&n)<<12|(63&r)<<6|63&a,l>65535&&l<1114112&&(s=l))}}null===s?(s=65533,i=1):s>65535&&(s-=65536,r.push(s>>>10&1023|55296),s=56320|1023&s),r.push(s),o+=i}return function(e){const t=e.length;if(t<=k)return String.fromCharCode.apply(String,e);let n="",r=0;for(;rr.length?(l.isBuffer(t)||(t=l.from(t)),t.copy(r,o)):Uint8Array.prototype.set.call(r,t,o);else{if(!l.isBuffer(t))throw new TypeError('"list" argument must be an Array of Buffers');t.copy(r,o)}o+=t.length}return r},l.byteLength=m,l.prototype._isBuffer=!0,l.prototype.swap16=function(){const e=this.length;if(e%2!=0)throw new RangeError("Buffer size must be a multiple of 16-bits");for(let t=0;tn&&(e+=" ... "),""},s&&(l.prototype[s]=l.prototype.inspect),l.prototype.compare=function(e,t,n,r,o){if(G(e,Uint8Array)&&(e=l.from(e,e.offset,e.byteLength)),!l.isBuffer(e))throw new TypeError('The "target" argument must be one of type Buffer or Uint8Array. Received type '+typeof e);if(void 0===t&&(t=0),void 0===n&&(n=e?e.length:0),void 0===r&&(r=0),void 0===o&&(o=this.length),t<0||n>e.length||r<0||o>this.length)throw new RangeError("out of range index");if(r>=o&&t>=n)return 0;if(r>=o)return-1;if(t>=n)return 1;if(this===e)return 0;let s=(o>>>=0)-(r>>>=0),i=(n>>>=0)-(t>>>=0);const a=Math.min(s,i),c=this.slice(r,o),u=e.slice(t,n);for(let e=0;e>>=0,isFinite(n)?(n>>>=0,void 0===r&&(r="utf8")):(r=n,n=void 0)}const o=this.length-t;if((void 0===n||n>o)&&(n=o),e.length>0&&(n<0||t<0)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");r||(r="utf8");let s=!1;for(;;)switch(r){case"hex":return w(this,e,t,n);case"utf8":case"utf-8":return E(this,e,t,n);case"ascii":case"latin1":case"binary":return x(this,e,t,n);case"base64":return S(this,e,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return _(this,e,t,n);default:if(s)throw new TypeError("Unknown encoding: "+r);r=(""+r).toLowerCase(),s=!0}},l.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};const k=4096;function A(e,t,n){let r="";n=Math.min(e.length,n);for(let o=t;or)&&(n=r);let o="";for(let r=t;rn)throw new RangeError("Trying to access beyond buffer length")}function T(e,t,n,r,o,s){if(!l.isBuffer(e))throw new TypeError('"buffer" argument must be a Buffer instance');if(t>o||te.length)throw new RangeError("Index out of range")}function R(e,t,n,r,o){U(t,r,o,e,n,7);let s=Number(t&BigInt(4294967295));e[n++]=s,s>>=8,e[n++]=s,s>>=8,e[n++]=s,s>>=8,e[n++]=s;let i=Number(t>>BigInt(32)&BigInt(4294967295));return e[n++]=i,i>>=8,e[n++]=i,i>>=8,e[n++]=i,i>>=8,e[n++]=i,n}function M(e,t,n,r,o){U(t,r,o,e,n,7);let s=Number(t&BigInt(4294967295));e[n+7]=s,s>>=8,e[n+6]=s,s>>=8,e[n+5]=s,s>>=8,e[n+4]=s;let i=Number(t>>BigInt(32)&BigInt(4294967295));return e[n+3]=i,i>>=8,e[n+2]=i,i>>=8,e[n+1]=i,i>>=8,e[n]=i,n+8}function D(e,t,n,r,o,s){if(n+r>e.length)throw new RangeError("Index out of range");if(n<0)throw new RangeError("Index out of range")}function F(e,t,n,r,s){return t=+t,n>>>=0,s||D(e,0,n,4),o.write(e,t,n,r,23,4),n+4}function L(e,t,n,r,s){return t=+t,n>>>=0,s||D(e,0,n,8),o.write(e,t,n,r,52,8),n+8}l.prototype.slice=function(e,t){const n=this.length;(e=~~e)<0?(e+=n)<0&&(e=0):e>n&&(e=n),(t=void 0===t?n:~~t)<0?(t+=n)<0&&(t=0):t>n&&(t=n),t>>=0,t>>>=0,n||I(e,t,this.length);let r=this[e],o=1,s=0;for(;++s>>=0,t>>>=0,n||I(e,t,this.length);let r=this[e+--t],o=1;for(;t>0&&(o*=256);)r+=this[e+--t]*o;return r},l.prototype.readUint8=l.prototype.readUInt8=function(e,t){return e>>>=0,t||I(e,1,this.length),this[e]},l.prototype.readUint16LE=l.prototype.readUInt16LE=function(e,t){return e>>>=0,t||I(e,2,this.length),this[e]|this[e+1]<<8},l.prototype.readUint16BE=l.prototype.readUInt16BE=function(e,t){return e>>>=0,t||I(e,2,this.length),this[e]<<8|this[e+1]},l.prototype.readUint32LE=l.prototype.readUInt32LE=function(e,t){return e>>>=0,t||I(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},l.prototype.readUint32BE=l.prototype.readUInt32BE=function(e,t){return e>>>=0,t||I(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},l.prototype.readBigUInt64LE=X((function(e){z(e>>>=0,"offset");const t=this[e],n=this[e+7];void 0!==t&&void 0!==n||V(e,this.length-8);const r=t+256*this[++e]+65536*this[++e]+this[++e]*2**24,o=this[++e]+256*this[++e]+65536*this[++e]+n*2**24;return BigInt(r)+(BigInt(o)<>>=0,"offset");const t=this[e],n=this[e+7];void 0!==t&&void 0!==n||V(e,this.length-8);const r=t*2**24+65536*this[++e]+256*this[++e]+this[++e],o=this[++e]*2**24+65536*this[++e]+256*this[++e]+n;return(BigInt(r)<>>=0,t>>>=0,n||I(e,t,this.length);let r=this[e],o=1,s=0;for(;++s=o&&(r-=Math.pow(2,8*t)),r},l.prototype.readIntBE=function(e,t,n){e>>>=0,t>>>=0,n||I(e,t,this.length);let r=t,o=1,s=this[e+--r];for(;r>0&&(o*=256);)s+=this[e+--r]*o;return o*=128,s>=o&&(s-=Math.pow(2,8*t)),s},l.prototype.readInt8=function(e,t){return e>>>=0,t||I(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},l.prototype.readInt16LE=function(e,t){e>>>=0,t||I(e,2,this.length);const n=this[e]|this[e+1]<<8;return 32768&n?4294901760|n:n},l.prototype.readInt16BE=function(e,t){e>>>=0,t||I(e,2,this.length);const n=this[e+1]|this[e]<<8;return 32768&n?4294901760|n:n},l.prototype.readInt32LE=function(e,t){return e>>>=0,t||I(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},l.prototype.readInt32BE=function(e,t){return e>>>=0,t||I(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},l.prototype.readBigInt64LE=X((function(e){z(e>>>=0,"offset");const t=this[e],n=this[e+7];void 0!==t&&void 0!==n||V(e,this.length-8);const r=this[e+4]+256*this[e+5]+65536*this[e+6]+(n<<24);return(BigInt(r)<>>=0,"offset");const t=this[e],n=this[e+7];void 0!==t&&void 0!==n||V(e,this.length-8);const r=(t<<24)+65536*this[++e]+256*this[++e]+this[++e];return(BigInt(r)<>>=0,t||I(e,4,this.length),o.read(this,e,!0,23,4)},l.prototype.readFloatBE=function(e,t){return e>>>=0,t||I(e,4,this.length),o.read(this,e,!1,23,4)},l.prototype.readDoubleLE=function(e,t){return e>>>=0,t||I(e,8,this.length),o.read(this,e,!0,52,8)},l.prototype.readDoubleBE=function(e,t){return e>>>=0,t||I(e,8,this.length),o.read(this,e,!1,52,8)},l.prototype.writeUintLE=l.prototype.writeUIntLE=function(e,t,n,r){if(e=+e,t>>>=0,n>>>=0,!r){T(this,e,t,n,Math.pow(2,8*n)-1,0)}let o=1,s=0;for(this[t]=255&e;++s>>=0,n>>>=0,!r){T(this,e,t,n,Math.pow(2,8*n)-1,0)}let o=n-1,s=1;for(this[t+o]=255&e;--o>=0&&(s*=256);)this[t+o]=e/s&255;return t+n},l.prototype.writeUint8=l.prototype.writeUInt8=function(e,t,n){return e=+e,t>>>=0,n||T(this,e,t,1,255,0),this[t]=255&e,t+1},l.prototype.writeUint16LE=l.prototype.writeUInt16LE=function(e,t,n){return e=+e,t>>>=0,n||T(this,e,t,2,65535,0),this[t]=255&e,this[t+1]=e>>>8,t+2},l.prototype.writeUint16BE=l.prototype.writeUInt16BE=function(e,t,n){return e=+e,t>>>=0,n||T(this,e,t,2,65535,0),this[t]=e>>>8,this[t+1]=255&e,t+2},l.prototype.writeUint32LE=l.prototype.writeUInt32LE=function(e,t,n){return e=+e,t>>>=0,n||T(this,e,t,4,4294967295,0),this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e,t+4},l.prototype.writeUint32BE=l.prototype.writeUInt32BE=function(e,t,n){return e=+e,t>>>=0,n||T(this,e,t,4,4294967295,0),this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e,t+4},l.prototype.writeBigUInt64LE=X((function(e,t=0){return R(this,e,t,BigInt(0),BigInt("0xffffffffffffffff"))})),l.prototype.writeBigUInt64BE=X((function(e,t=0){return M(this,e,t,BigInt(0),BigInt("0xffffffffffffffff"))})),l.prototype.writeIntLE=function(e,t,n,r){if(e=+e,t>>>=0,!r){const r=Math.pow(2,8*n-1);T(this,e,t,n,r-1,-r)}let o=0,s=1,i=0;for(this[t]=255&e;++o>0)-i&255;return t+n},l.prototype.writeIntBE=function(e,t,n,r){if(e=+e,t>>>=0,!r){const r=Math.pow(2,8*n-1);T(this,e,t,n,r-1,-r)}let o=n-1,s=1,i=0;for(this[t+o]=255&e;--o>=0&&(s*=256);)e<0&&0===i&&0!==this[t+o+1]&&(i=1),this[t+o]=(e/s>>0)-i&255;return t+n},l.prototype.writeInt8=function(e,t,n){return e=+e,t>>>=0,n||T(this,e,t,1,127,-128),e<0&&(e=255+e+1),this[t]=255&e,t+1},l.prototype.writeInt16LE=function(e,t,n){return e=+e,t>>>=0,n||T(this,e,t,2,32767,-32768),this[t]=255&e,this[t+1]=e>>>8,t+2},l.prototype.writeInt16BE=function(e,t,n){return e=+e,t>>>=0,n||T(this,e,t,2,32767,-32768),this[t]=e>>>8,this[t+1]=255&e,t+2},l.prototype.writeInt32LE=function(e,t,n){return e=+e,t>>>=0,n||T(this,e,t,4,2147483647,-2147483648),this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24,t+4},l.prototype.writeInt32BE=function(e,t,n){return e=+e,t>>>=0,n||T(this,e,t,4,2147483647,-2147483648),e<0&&(e=4294967295+e+1),this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e,t+4},l.prototype.writeBigInt64LE=X((function(e,t=0){return R(this,e,t,-BigInt("0x8000000000000000"),BigInt("0x7fffffffffffffff"))})),l.prototype.writeBigInt64BE=X((function(e,t=0){return M(this,e,t,-BigInt("0x8000000000000000"),BigInt("0x7fffffffffffffff"))})),l.prototype.writeFloatLE=function(e,t,n){return F(this,e,t,!0,n)},l.prototype.writeFloatBE=function(e,t,n){return F(this,e,t,!1,n)},l.prototype.writeDoubleLE=function(e,t,n){return L(this,e,t,!0,n)},l.prototype.writeDoubleBE=function(e,t,n){return L(this,e,t,!1,n)},l.prototype.copy=function(e,t,n,r){if(!l.isBuffer(e))throw new TypeError("argument should be a Buffer");if(n||(n=0),r||0===r||(r=this.length),t>=e.length&&(t=e.length),t||(t=0),r>0&&r=this.length)throw new RangeError("Index out of range");if(r<0)throw new RangeError("sourceEnd out of bounds");r>this.length&&(r=this.length),e.length-t>>=0,n=void 0===n?this.length:n>>>0,e||(e=0),"number"==typeof e)for(o=t;o=r+4;n-=3)t=`_${e.slice(n-3,n)}${t}`;return`${e.slice(0,n)}${t}`}function U(e,t,n,r,o,s){if(e>n||e3?0===t||t===BigInt(0)?`>= 0${r} and < 2${r} ** ${8*(s+1)}${r}`:`>= -(2${r} ** ${8*(s+1)-1}${r}) and < 2 ** ${8*(s+1)-1}${r}`:`>= ${t}${r} and <= ${n}${r}`,new B.ERR_OUT_OF_RANGE("value",o,e)}!function(e,t,n){z(t,"offset"),void 0!==e[t]&&void 0!==e[t+n]||V(t,e.length-(n+1))}(r,o,s)}function z(e,t){if("number"!=typeof e)throw new B.ERR_INVALID_ARG_TYPE(t,"number",e)}function V(e,t,n){if(Math.floor(e)!==e)throw z(e,n),new B.ERR_OUT_OF_RANGE(n||"offset","an integer",e);if(t<0)throw new B.ERR_BUFFER_OUT_OF_BOUNDS;throw new B.ERR_OUT_OF_RANGE(n||"offset",`>= ${n?1:0} and <= ${t}`,e)}$("ERR_BUFFER_OUT_OF_BOUNDS",(function(e){return e?`${e} is outside of buffer bounds`:"Attempt to access memory outside buffer bounds"}),RangeError),$("ERR_INVALID_ARG_TYPE",(function(e,t){return`The "${e}" argument must be of type number. Received type ${typeof t}`}),TypeError),$("ERR_OUT_OF_RANGE",(function(e,t,n){let r=`The value of "${e}" is out of range.`,o=n;return Number.isInteger(n)&&Math.abs(n)>2**32?o=q(String(n)):"bigint"==typeof n&&(o=String(n),(n>BigInt(2)**BigInt(32)||n<-(BigInt(2)**BigInt(32)))&&(o=q(o)),o+="n"),r+=` It must be ${t}. Received ${o}`,r}),RangeError);const W=/[^+/0-9A-Za-z-_]/g;function J(e,t){let n;t=t||1/0;const r=e.length;let o=null;const s=[];for(let i=0;i55295&&n<57344){if(!o){if(n>56319){(t-=3)>-1&&s.push(239,191,189);continue}if(i+1===r){(t-=3)>-1&&s.push(239,191,189);continue}o=n;continue}if(n<56320){(t-=3)>-1&&s.push(239,191,189),o=n;continue}n=65536+(o-55296<<10|n-56320)}else o&&(t-=3)>-1&&s.push(239,191,189);if(o=null,n<128){if((t-=1)<0)break;s.push(n)}else if(n<2048){if((t-=2)<0)break;s.push(n>>6|192,63&n|128)}else if(n<65536){if((t-=3)<0)break;s.push(n>>12|224,n>>6&63|128,63&n|128)}else{if(!(n<1114112))throw new Error("Invalid code point");if((t-=4)<0)break;s.push(n>>18|240,n>>12&63|128,n>>6&63|128,63&n|128)}}return s}function K(e){return r.toByteArray(function(e){if((e=(e=e.split("=")[0]).trim().replace(W,"")).length<2)return"";for(;e.length%4!=0;)e+="=";return e}(e))}function H(e,t,n,r){let o;for(o=0;o=t.length||o>=e.length);++o)t[o+n]=e[o];return o}function G(e,t){return e instanceof t||null!=e&&null!=e.constructor&&null!=e.constructor.name&&e.constructor.name===t.name}function Z(e){return e!=e}const Y=function(){const e="0123456789abcdef",t=new Array(256);for(let n=0;n<16;++n){const r=16*n;for(let o=0;o<16;++o)t[r+o]=e[n]+e[o]}return t}();function X(e){return"undefined"==typeof BigInt?Q:e}function Q(){throw new Error("BigInt not supported")}},21924:(e,t,n)=>{"use strict";var r=n(40210),o=n(55559),s=o(r("String.prototype.indexOf"));e.exports=function(e,t){var n=r(e,!!t);return"function"==typeof n&&s(e,".prototype.")>-1?o(n):n}},55559:(e,t,n)=>{"use strict";var r=n(58612),o=n(40210),s=o("%Function.prototype.apply%"),i=o("%Function.prototype.call%"),a=o("%Reflect.apply%",!0)||r.call(i,s),l=o("%Object.getOwnPropertyDescriptor%",!0),c=o("%Object.defineProperty%",!0),u=o("%Math.max%");if(c)try{c({},"a",{value:1})}catch(e){c=null}e.exports=function(e){var t=a(r,i,arguments);l&&c&&(l(t,"length").configurable&&c(t,"length",{value:1+u(0,e.length-(arguments.length-1))}));return t};var p=function(){return a(r,s,arguments)};c?c(e.exports,"apply",{value:p}):e.exports.apply=p},94184:(e,t)=>{var n;!function(){"use strict";var r={}.hasOwnProperty;function o(){for(var e=[],t=0;t{"use strict";t.parse=function(e,t){if("string"!=typeof e)throw new TypeError("argument str must be a string");var n={},r=(t||{}).decode||o,s=0;for(;s{"use strict";var r=n(11742),o={"text/plain":"Text","text/html":"Url",default:"Text"};e.exports=function(e,t){var n,s,i,a,l,c,u=!1;t||(t={}),n=t.debug||!1;try{if(i=r(),a=document.createRange(),l=document.getSelection(),(c=document.createElement("span")).textContent=e,c.ariaHidden="true",c.style.all="unset",c.style.position="fixed",c.style.top=0,c.style.clip="rect(0, 0, 0, 0)",c.style.whiteSpace="pre",c.style.webkitUserSelect="text",c.style.MozUserSelect="text",c.style.msUserSelect="text",c.style.userSelect="text",c.addEventListener("copy",(function(r){if(r.stopPropagation(),t.format)if(r.preventDefault(),void 0===r.clipboardData){n&&console.warn("unable to use e.clipboardData"),n&&console.warn("trying IE specific stuff"),window.clipboardData.clearData();var s=o[t.format]||o.default;window.clipboardData.setData(s,e)}else r.clipboardData.clearData(),r.clipboardData.setData(t.format,e);t.onCopy&&(r.preventDefault(),t.onCopy(r.clipboardData))})),document.body.appendChild(c),a.selectNodeContents(c),l.addRange(a),!document.execCommand("copy"))throw new Error("copy command was unsuccessful");u=!0}catch(r){n&&console.error("unable to copy using execCommand: ",r),n&&console.warn("trying IE specific stuff");try{window.clipboardData.setData(t.format||"text",e),t.onCopy&&t.onCopy(window.clipboardData),u=!0}catch(r){n&&console.error("unable to copy using clipboardData: ",r),n&&console.error("falling back to prompt"),s=function(e){var t=(/mac os x/i.test(navigator.userAgent)?"⌘":"Ctrl")+"+C";return e.replace(/#{\s*key\s*}/g,t)}("message"in t?t.message:"Copy to clipboard: #{key}, Enter"),window.prompt(s,e)}}finally{l&&("function"==typeof l.removeRange?l.removeRange(a):l.removeAllRanges()),c&&document.body.removeChild(c),i()}return u}},90093:(e,t,n)=>{var r=n(28196);e.exports=r},3688:(e,t,n)=>{var r=n(11955);e.exports=r},83838:(e,t,n)=>{var r=n(46279);e.exports=r},15684:(e,t,n)=>{var r=n(19373);e.exports=r},81331:(e,t,n)=>{var r=n(52759);e.exports=r},65362:(e,t,n)=>{var r=n(63383);e.exports=r},91254:(e,t,n)=>{var r=n(57396);e.exports=r},43536:(e,t,n)=>{var r=n(41910);e.exports=r},37331:(e,t,n)=>{var r=n(79427);e.exports=r},68522:(e,t,n)=>{var r=n(62857);e.exports=r},73151:(e,t,n)=>{var r=n(9534);e.exports=r},45012:(e,t,n)=>{var r=n(23059);e.exports=r},80281:(e,t,n)=>{var r=n(92547);n(97522),n(43975),n(45414),e.exports=r},40031:(e,t,n)=>{var r=n(46509);e.exports=r},17487:(e,t,n)=>{var r=n(35774);e.exports=r},54493:(e,t,n)=>{n(77971),n(53242);var r=n(54058);e.exports=r.Array.from},24034:(e,t,n)=>{n(92737);var r=n(54058);e.exports=r.Array.isArray},15367:(e,t,n)=>{n(85906);var r=n(35703);e.exports=r("Array").concat},12710:(e,t,n)=>{n(66274),n(55967);var r=n(35703);e.exports=r("Array").entries},51459:(e,t,n)=>{n(48851);var r=n(35703);e.exports=r("Array").every},6172:(e,t,n)=>{n(80290);var r=n(35703);e.exports=r("Array").fill},62383:(e,t,n)=>{n(21501);var r=n(35703);e.exports=r("Array").filter},60009:(e,t,n)=>{n(44929);var r=n(35703);e.exports=r("Array").findIndex},17671:(e,t,n)=>{n(80833);var r=n(35703);e.exports=r("Array").find},99324:(e,t,n)=>{n(2437);var r=n(35703);e.exports=r("Array").forEach},80991:(e,t,n)=>{n(97690);var r=n(35703);e.exports=r("Array").includes},8700:(e,t,n)=>{n(99076);var r=n(35703);e.exports=r("Array").indexOf},95909:(e,t,n)=>{n(66274),n(55967);var r=n(35703);e.exports=r("Array").keys},6442:(e,t,n)=>{n(75915);var r=n(35703);e.exports=r("Array").lastIndexOf},23866:(e,t,n)=>{n(68787);var r=n(35703);e.exports=r("Array").map},9896:(e,t,n)=>{n(48528);var r=n(35703);e.exports=r("Array").push},52999:(e,t,n)=>{n(81876);var r=n(35703);e.exports=r("Array").reduce},24900:(e,t,n)=>{n(60186);var r=n(35703);e.exports=r("Array").slice},3824:(e,t,n)=>{n(36026);var r=n(35703);e.exports=r("Array").some},2948:(e,t,n)=>{n(4115);var r=n(35703);e.exports=r("Array").sort},78209:(e,t,n)=>{n(98611);var r=n(35703);e.exports=r("Array").splice},14423:(e,t,n)=>{n(66274),n(55967);var r=n(35703);e.exports=r("Array").values},81103:(e,t,n)=>{n(95160);var r=n(54058);e.exports=r.Date.now},27700:(e,t,n)=>{n(73381);var r=n(35703);e.exports=r("Function").bind},16246:(e,t,n)=>{var r=n(7046),o=n(27700),s=Function.prototype;e.exports=function(e){var t=e.bind;return e===s||r(s,e)&&t===s.bind?o:t}},56043:(e,t,n)=>{var r=n(7046),o=n(15367),s=Array.prototype;e.exports=function(e){var t=e.concat;return e===s||r(s,e)&&t===s.concat?o:t}},13160:(e,t,n)=>{var r=n(7046),o=n(51459),s=Array.prototype;e.exports=function(e){var t=e.every;return e===s||r(s,e)&&t===s.every?o:t}},80446:(e,t,n)=>{var r=n(7046),o=n(6172),s=Array.prototype;e.exports=function(e){var t=e.fill;return e===s||r(s,e)&&t===s.fill?o:t}},2480:(e,t,n)=>{var r=n(7046),o=n(62383),s=Array.prototype;e.exports=function(e){var t=e.filter;return e===s||r(s,e)&&t===s.filter?o:t}},7147:(e,t,n)=>{var r=n(7046),o=n(60009),s=Array.prototype;e.exports=function(e){var t=e.findIndex;return e===s||r(s,e)&&t===s.findIndex?o:t}},32236:(e,t,n)=>{var r=n(7046),o=n(17671),s=Array.prototype;e.exports=function(e){var t=e.find;return e===s||r(s,e)&&t===s.find?o:t}},58557:(e,t,n)=>{var r=n(7046),o=n(80991),s=n(21631),i=Array.prototype,a=String.prototype;e.exports=function(e){var t=e.includes;return e===i||r(i,e)&&t===i.includes?o:"string"==typeof e||e===a||r(a,e)&&t===a.includes?s:t}},34570:(e,t,n)=>{var r=n(7046),o=n(8700),s=Array.prototype;e.exports=function(e){var t=e.indexOf;return e===s||r(s,e)&&t===s.indexOf?o:t}},57564:(e,t,n)=>{var r=n(7046),o=n(6442),s=Array.prototype;e.exports=function(e){var t=e.lastIndexOf;return e===s||r(s,e)&&t===s.lastIndexOf?o:t}},88287:(e,t,n)=>{var r=n(7046),o=n(23866),s=Array.prototype;e.exports=function(e){var t=e.map;return e===s||r(s,e)&&t===s.map?o:t}},93993:(e,t,n)=>{var r=n(7046),o=n(9896),s=Array.prototype;e.exports=function(e){var t=e.push;return e===s||r(s,e)&&t===s.push?o:t}},68025:(e,t,n)=>{var r=n(7046),o=n(52999),s=Array.prototype;e.exports=function(e){var t=e.reduce;return e===s||r(s,e)&&t===s.reduce?o:t}},59257:(e,t,n)=>{var r=n(7046),o=n(80454),s=String.prototype;e.exports=function(e){var t=e.repeat;return"string"==typeof e||e===s||r(s,e)&&t===s.repeat?o:t}},69601:(e,t,n)=>{var r=n(7046),o=n(24900),s=Array.prototype;e.exports=function(e){var t=e.slice;return e===s||r(s,e)&&t===s.slice?o:t}},28299:(e,t,n)=>{var r=n(7046),o=n(3824),s=Array.prototype;e.exports=function(e){var t=e.some;return e===s||r(s,e)&&t===s.some?o:t}},69355:(e,t,n)=>{var r=n(7046),o=n(2948),s=Array.prototype;e.exports=function(e){var t=e.sort;return e===s||r(s,e)&&t===s.sort?o:t}},18339:(e,t,n)=>{var r=n(7046),o=n(78209),s=Array.prototype;e.exports=function(e){var t=e.splice;return e===s||r(s,e)&&t===s.splice?o:t}},71611:(e,t,n)=>{var r=n(7046),o=n(3269),s=String.prototype;e.exports=function(e){var t=e.startsWith;return"string"==typeof e||e===s||r(s,e)&&t===s.startsWith?o:t}},62774:(e,t,n)=>{var r=n(7046),o=n(13348),s=String.prototype;e.exports=function(e){var t=e.trim;return"string"==typeof e||e===s||r(s,e)&&t===s.trim?o:t}},84426:(e,t,n)=>{n(32619);var r=n(54058),o=n(79730);r.JSON||(r.JSON={stringify:JSON.stringify}),e.exports=function(e,t,n){return o(r.JSON.stringify,null,arguments)}},91018:(e,t,n)=>{n(66274),n(37501),n(55967),n(77971);var r=n(54058);e.exports=r.Map},97849:(e,t,n)=>{n(54973),e.exports=Math.pow(2,-52)},3820:(e,t,n)=>{n(30800);var r=n(54058);e.exports=r.Number.isInteger},45999:(e,t,n)=>{n(49221);var r=n(54058);e.exports=r.Object.assign},7702:(e,t,n)=>{n(74979);var r=n(54058).Object,o=e.exports=function(e,t){return r.defineProperties(e,t)};r.defineProperties.sham&&(o.sham=!0)},48171:(e,t,n)=>{n(86450);var r=n(54058).Object,o=e.exports=function(e,t,n){return r.defineProperty(e,t,n)};r.defineProperty.sham&&(o.sham=!0)},73081:(e,t,n)=>{n(94366);var r=n(54058);e.exports=r.Object.entries},7699:(e,t,n)=>{n(66274),n(28387);var r=n(54058);e.exports=r.Object.fromEntries},286:(e,t,n)=>{n(46924);var r=n(54058).Object,o=e.exports=function(e,t){return r.getOwnPropertyDescriptor(e,t)};r.getOwnPropertyDescriptor.sham&&(o.sham=!0)},92766:(e,t,n)=>{n(88482);var r=n(54058);e.exports=r.Object.getOwnPropertyDescriptors},30498:(e,t,n)=>{n(35824);var r=n(54058);e.exports=r.Object.getOwnPropertySymbols},48494:(e,t,n)=>{n(21724);var r=n(54058);e.exports=r.Object.keys},98430:(e,t,n)=>{n(26614);var r=n(54058);e.exports=r.Object.values},52956:(e,t,n)=>{n(47627),n(66274),n(55967),n(98881),n(4560),n(91302),n(44349),n(77971);var r=n(54058);e.exports=r.Promise},76998:(e,t,n)=>{n(66274),n(55967),n(69008),n(77971);var r=n(54058);e.exports=r.Set},97089:(e,t,n)=>{n(74679);var r=n(54058);e.exports=r.String.raw},21631:(e,t,n)=>{n(11035);var r=n(35703);e.exports=r("String").includes},80454:(e,t,n)=>{n(60986);var r=n(35703);e.exports=r("String").repeat},3269:(e,t,n)=>{n(94761);var r=n(35703);e.exports=r("String").startsWith},13348:(e,t,n)=>{n(57398);var r=n(35703);e.exports=r("String").trim},57473:(e,t,n)=>{n(85906),n(55967),n(35824),n(8555),n(52615),n(21732),n(35903),n(1825),n(28394),n(45915),n(61766),n(62737),n(89911),n(74315),n(63131),n(64714),n(70659),n(69120),n(79413),n(1502);var r=n(54058);e.exports=r.Symbol},24227:(e,t,n)=>{n(66274),n(55967),n(77971),n(1825);var r=n(11477);e.exports=r.f("iterator")},62978:(e,t,n)=>{n(18084),n(63131);var r=n(11477);e.exports=r.f("toPrimitive")},32304:(e,t,n)=>{n(66274),n(55967),n(54334);var r=n(54058);e.exports=r.WeakMap},29567:(e,t,n)=>{n(66274),n(55967),n(1773);var r=n(54058);e.exports=r.WeakSet},14122:(e,t,n)=>{e.exports=n(89097)},44442:(e,t,n)=>{e.exports=n(51675)},57152:(e,t,n)=>{e.exports=n(82507)},69447:(e,t,n)=>{e.exports=n(628)},1449:(e,t,n)=>{e.exports=n(34501)},60269:(e,t,n)=>{e.exports=n(76936)},70573:(e,t,n)=>{e.exports=n(18180)},73685:(e,t,n)=>{e.exports=n(80621)},27533:(e,t,n)=>{e.exports=n(22948)},39057:(e,t,n)=>{e.exports=n(82108)},84710:(e,t,n)=>{e.exports=n(14058)},93799:(e,t,n)=>{e.exports=n(92093)},86600:(e,t,n)=>{e.exports=n(52201)},9759:(e,t,n)=>{e.exports=n(27398)},71384:(e,t,n)=>{e.exports=n(26189)},89097:(e,t,n)=>{var r=n(90093);e.exports=r},51675:(e,t,n)=>{var r=n(3688);e.exports=r},82507:(e,t,n)=>{var r=n(83838);e.exports=r},628:(e,t,n)=>{var r=n(15684);e.exports=r},34501:(e,t,n)=>{var r=n(81331);e.exports=r},76936:(e,t,n)=>{var r=n(65362);e.exports=r},18180:(e,t,n)=>{var r=n(91254);e.exports=r},80621:(e,t,n)=>{var r=n(43536);e.exports=r},22948:(e,t,n)=>{var r=n(37331);e.exports=r},82108:(e,t,n)=>{var r=n(68522);e.exports=r},14058:(e,t,n)=>{var r=n(73151);e.exports=r},92093:(e,t,n)=>{var r=n(45012);e.exports=r},52201:(e,t,n)=>{var r=n(80281);n(28783),n(97618),n(6989),n(65799),n(46774),n(22731),n(85605),n(31943),n(80620),n(36172),e.exports=r},27398:(e,t,n)=>{var r=n(40031);e.exports=r},26189:(e,t,n)=>{var r=n(17487);e.exports=r},24883:(e,t,n)=>{var r=n(57475),o=n(69826),s=TypeError;e.exports=function(e){if(r(e))return e;throw s(o(e)+" is not a function")}},174:(e,t,n)=>{var r=n(24284),o=n(69826),s=TypeError;e.exports=function(e){if(r(e))return e;throw s(o(e)+" is not a constructor")}},11851:(e,t,n)=>{var r=n(57475),o=String,s=TypeError;e.exports=function(e){if("object"==typeof e||r(e))return e;throw s("Can't set "+o(e)+" as a prototype")}},18479:e=>{e.exports=function(){}},5743:(e,t,n)=>{var r=n(7046),o=TypeError;e.exports=function(e,t){if(r(t,e))return e;throw o("Incorrect invocation")}},96059:(e,t,n)=>{var r=n(10941),o=String,s=TypeError;e.exports=function(e){if(r(e))return e;throw s(o(e)+" is not an object")}},97135:(e,t,n)=>{var r=n(95981);e.exports=r((function(){if("function"==typeof ArrayBuffer){var e=new ArrayBuffer(8);Object.isExtensible(e)&&Object.defineProperty(e,"a",{value:8})}}))},91860:(e,t,n)=>{"use strict";var r=n(89678),o=n(59413),s=n(10623);e.exports=function(e){for(var t=r(this),n=s(t),i=arguments.length,a=o(i>1?arguments[1]:void 0,n),l=i>2?arguments[2]:void 0,c=void 0===l?n:o(l,n);c>a;)t[a++]=e;return t}},56837:(e,t,n)=>{"use strict";var r=n(3610).forEach,o=n(34194)("forEach");e.exports=o?[].forEach:function(e){return r(this,e,arguments.length>1?arguments[1]:void 0)}},11354:(e,t,n)=>{"use strict";var r=n(86843),o=n(78834),s=n(89678),i=n(75196),a=n(6782),l=n(24284),c=n(10623),u=n(55449),p=n(53476),h=n(22902),f=Array;e.exports=function(e){var t=s(e),n=l(this),d=arguments.length,m=d>1?arguments[1]:void 0,g=void 0!==m;g&&(m=r(m,d>2?arguments[2]:void 0));var y,v,b,w,E,x,S=h(t),_=0;if(!S||this===f&&a(S))for(y=c(t),v=n?new this(y):f(y);y>_;_++)x=g?m(t[_],_):t[_],u(v,_,x);else for(E=(w=p(t,S)).next,v=n?new this:[];!(b=o(E,w)).done;_++)x=g?i(w,m,[b.value,_],!0):b.value,u(v,_,x);return v.length=_,v}},31692:(e,t,n)=>{var r=n(74529),o=n(59413),s=n(10623),i=function(e){return function(t,n,i){var a,l=r(t),c=s(l),u=o(i,c);if(e&&n!=n){for(;c>u;)if((a=l[u++])!=a)return!0}else for(;c>u;u++)if((e||u in l)&&l[u]===n)return e||u||0;return!e&&-1}};e.exports={includes:i(!0),indexOf:i(!1)}},3610:(e,t,n)=>{var r=n(86843),o=n(95329),s=n(37026),i=n(89678),a=n(10623),l=n(64692),c=o([].push),u=function(e){var t=1==e,n=2==e,o=3==e,u=4==e,p=6==e,h=7==e,f=5==e||p;return function(d,m,g,y){for(var v,b,w=i(d),E=s(w),x=r(m,g),S=a(E),_=0,j=y||l,O=t?j(d,S):n||h?j(d,0):void 0;S>_;_++)if((f||_ in E)&&(b=x(v=E[_],_,w),e))if(t)O[_]=b;else if(b)switch(e){case 3:return!0;case 5:return v;case 6:return _;case 2:c(O,v)}else switch(e){case 4:return!1;case 7:c(O,v)}return p?-1:o||u?u:O}};e.exports={forEach:u(0),map:u(1),filter:u(2),some:u(3),every:u(4),find:u(5),findIndex:u(6),filterReject:u(7)}},67145:(e,t,n)=>{"use strict";var r=n(79730),o=n(74529),s=n(62435),i=n(10623),a=n(34194),l=Math.min,c=[].lastIndexOf,u=!!c&&1/[1].lastIndexOf(1,-0)<0,p=a("lastIndexOf"),h=u||!p;e.exports=h?function(e){if(u)return r(c,this,arguments)||0;var t=o(this),n=i(t),a=n-1;for(arguments.length>1&&(a=l(a,s(arguments[1]))),a<0&&(a=n+a);a>=0;a--)if(a in t&&t[a]===e)return a||0;return-1}:c},50568:(e,t,n)=>{var r=n(95981),o=n(99813),s=n(53385),i=o("species");e.exports=function(e){return s>=51||!r((function(){var t=[];return(t.constructor={})[i]=function(){return{foo:1}},1!==t[e](Boolean).foo}))}},34194:(e,t,n)=>{"use strict";var r=n(95981);e.exports=function(e,t){var n=[][e];return!!n&&r((function(){n.call(null,t||function(){return 1},1)}))}},46499:(e,t,n)=>{var r=n(24883),o=n(89678),s=n(37026),i=n(10623),a=TypeError,l=function(e){return function(t,n,l,c){r(n);var u=o(t),p=s(u),h=i(u),f=e?h-1:0,d=e?-1:1;if(l<2)for(;;){if(f in p){c=p[f],f+=d;break}if(f+=d,e?f<0:h<=f)throw a("Reduce of empty array with no initial value")}for(;e?f>=0:h>f;f+=d)f in p&&(c=n(c,p[f],f,u));return c}};e.exports={left:l(!1),right:l(!0)}},89779:(e,t,n)=>{"use strict";var r=n(55746),o=n(1052),s=TypeError,i=Object.getOwnPropertyDescriptor,a=r&&!function(){if(void 0!==this)return!0;try{Object.defineProperty([],"length",{writable:!1}).length=1}catch(e){return e instanceof TypeError}}();e.exports=a?function(e,t){if(o(e)&&!i(e,"length").writable)throw s("Cannot set read only .length");return e.length=t}:function(e,t){return e.length=t}},15790:(e,t,n)=>{var r=n(59413),o=n(10623),s=n(55449),i=Array,a=Math.max;e.exports=function(e,t,n){for(var l=o(e),c=r(t,l),u=r(void 0===n?l:n,l),p=i(a(u-c,0)),h=0;c{var r=n(95329);e.exports=r([].slice)},61388:(e,t,n)=>{var r=n(15790),o=Math.floor,s=function(e,t){var n=e.length,l=o(n/2);return n<8?i(e,t):a(e,s(r(e,0,l),t),s(r(e,l),t),t)},i=function(e,t){for(var n,r,o=e.length,s=1;s0;)e[r]=e[--r];r!==s++&&(e[r]=n)}return e},a=function(e,t,n,r){for(var o=t.length,s=n.length,i=0,a=0;i{var r=n(1052),o=n(24284),s=n(10941),i=n(99813)("species"),a=Array;e.exports=function(e){var t;return r(e)&&(t=e.constructor,(o(t)&&(t===a||r(t.prototype))||s(t)&&null===(t=t[i]))&&(t=void 0)),void 0===t?a:t}},64692:(e,t,n)=>{var r=n(5693);e.exports=function(e,t){return new(r(e))(0===t?0:t)}},75196:(e,t,n)=>{var r=n(96059),o=n(7609);e.exports=function(e,t,n,s){try{return s?t(r(n)[0],n[1]):t(n)}catch(t){o(e,"throw",t)}}},21385:(e,t,n)=>{var r=n(99813)("iterator"),o=!1;try{var s=0,i={next:function(){return{done:!!s++}},return:function(){o=!0}};i[r]=function(){return this},Array.from(i,(function(){throw 2}))}catch(e){}e.exports=function(e,t){if(!t&&!o)return!1;var n=!1;try{var s={};s[r]=function(){return{next:function(){return{done:n=!0}}}},e(s)}catch(e){}return n}},82532:(e,t,n)=>{var r=n(95329),o=r({}.toString),s=r("".slice);e.exports=function(e){return s(o(e),8,-1)}},9697:(e,t,n)=>{var r=n(22885),o=n(57475),s=n(82532),i=n(99813)("toStringTag"),a=Object,l="Arguments"==s(function(){return arguments}());e.exports=r?s:function(e){var t,n,r;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(n=function(e,t){try{return e[t]}catch(e){}}(t=a(e),i))?n:l?s(t):"Object"==(r=s(t))&&o(t.callee)?"Arguments":r}},85616:(e,t,n)=>{"use strict";var r=n(29290),o=n(29202),s=n(94380),i=n(86843),a=n(5743),l=n(82119),c=n(93091),u=n(75105),p=n(23538),h=n(94431),f=n(55746),d=n(21647).fastKey,m=n(45402),g=m.set,y=m.getterFor;e.exports={getConstructor:function(e,t,n,u){var p=e((function(e,o){a(e,h),g(e,{type:t,index:r(null),first:void 0,last:void 0,size:0}),f||(e.size=0),l(o)||c(o,e[u],{that:e,AS_ENTRIES:n})})),h=p.prototype,m=y(t),v=function(e,t,n){var r,o,s=m(e),i=b(e,t);return i?i.value=n:(s.last=i={index:o=d(t,!0),key:t,value:n,previous:r=s.last,next:void 0,removed:!1},s.first||(s.first=i),r&&(r.next=i),f?s.size++:e.size++,"F"!==o&&(s.index[o]=i)),e},b=function(e,t){var n,r=m(e),o=d(t);if("F"!==o)return r.index[o];for(n=r.first;n;n=n.next)if(n.key==t)return n};return s(h,{clear:function(){for(var e=m(this),t=e.index,n=e.first;n;)n.removed=!0,n.previous&&(n.previous=n.previous.next=void 0),delete t[n.index],n=n.next;e.first=e.last=void 0,f?e.size=0:this.size=0},delete:function(e){var t=this,n=m(t),r=b(t,e);if(r){var o=r.next,s=r.previous;delete n.index[r.index],r.removed=!0,s&&(s.next=o),o&&(o.previous=s),n.first==r&&(n.first=o),n.last==r&&(n.last=s),f?n.size--:t.size--}return!!r},forEach:function(e){for(var t,n=m(this),r=i(e,arguments.length>1?arguments[1]:void 0);t=t?t.next:n.first;)for(r(t.value,t.key,this);t&&t.removed;)t=t.previous},has:function(e){return!!b(this,e)}}),s(h,n?{get:function(e){var t=b(this,e);return t&&t.value},set:function(e,t){return v(this,0===e?0:e,t)}}:{add:function(e){return v(this,e=0===e?0:e,e)}}),f&&o(h,"size",{configurable:!0,get:function(){return m(this).size}}),p},setStrong:function(e,t,n){var r=t+" Iterator",o=y(t),s=y(r);u(e,t,(function(e,t){g(this,{type:r,target:e,state:o(e),kind:t,last:void 0})}),(function(){for(var e=s(this),t=e.kind,n=e.last;n&&n.removed;)n=n.previous;return e.target&&(e.last=n=n?n.next:e.state.first)?p("keys"==t?n.key:"values"==t?n.value:[n.key,n.value],!1):(e.target=void 0,p(void 0,!0))}),n?"entries":"values",!n,!0),h(t)}}},8850:(e,t,n)=>{"use strict";var r=n(95329),o=n(94380),s=n(21647).getWeakData,i=n(5743),a=n(96059),l=n(82119),c=n(10941),u=n(93091),p=n(3610),h=n(90953),f=n(45402),d=f.set,m=f.getterFor,g=p.find,y=p.findIndex,v=r([].splice),b=0,w=function(e){return e.frozen||(e.frozen=new E)},E=function(){this.entries=[]},x=function(e,t){return g(e.entries,(function(e){return e[0]===t}))};E.prototype={get:function(e){var t=x(this,e);if(t)return t[1]},has:function(e){return!!x(this,e)},set:function(e,t){var n=x(this,e);n?n[1]=t:this.entries.push([e,t])},delete:function(e){var t=y(this.entries,(function(t){return t[0]===e}));return~t&&v(this.entries,t,1),!!~t}},e.exports={getConstructor:function(e,t,n,r){var p=e((function(e,o){i(e,f),d(e,{type:t,id:b++,frozen:void 0}),l(o)||u(o,e[r],{that:e,AS_ENTRIES:n})})),f=p.prototype,g=m(t),y=function(e,t,n){var r=g(e),o=s(a(t),!0);return!0===o?w(r).set(t,n):o[r.id]=n,e};return o(f,{delete:function(e){var t=g(this);if(!c(e))return!1;var n=s(e);return!0===n?w(t).delete(e):n&&h(n,t.id)&&delete n[t.id]},has:function(e){var t=g(this);if(!c(e))return!1;var n=s(e);return!0===n?w(t).has(e):n&&h(n,t.id)}}),o(f,n?{get:function(e){var t=g(this);if(c(e)){var n=s(e);return!0===n?w(t).get(e):n?n[t.id]:void 0}},set:function(e,t){return y(this,e,t)}}:{add:function(e){return y(this,e,!0)}}),p}}},24683:(e,t,n)=>{"use strict";var r=n(76887),o=n(21899),s=n(21647),i=n(95981),a=n(32029),l=n(93091),c=n(5743),u=n(57475),p=n(10941),h=n(90904),f=n(65988).f,d=n(3610).forEach,m=n(55746),g=n(45402),y=g.set,v=g.getterFor;e.exports=function(e,t,n){var g,b=-1!==e.indexOf("Map"),w=-1!==e.indexOf("Weak"),E=b?"set":"add",x=o[e],S=x&&x.prototype,_={};if(m&&u(x)&&(w||S.forEach&&!i((function(){(new x).entries().next()})))){var j=(g=t((function(t,n){y(c(t,j),{type:e,collection:new x}),null!=n&&l(n,t[E],{that:t,AS_ENTRIES:b})}))).prototype,O=v(e);d(["add","clear","delete","forEach","get","has","set","keys","values","entries"],(function(e){var t="add"==e||"set"==e;!(e in S)||w&&"clear"==e||a(j,e,(function(n,r){var o=O(this).collection;if(!t&&w&&!p(n))return"get"==e&&void 0;var s=o[e](0===n?0:n,r);return t?this:s}))})),w||f(j,"size",{configurable:!0,get:function(){return O(this).collection.size}})}else g=n.getConstructor(t,e,b,E),s.enable();return h(g,e,!1,!0),_[e]=g,r({global:!0,forced:!0},_),w||n.setStrong(g,e,b),g}},23489:(e,t,n)=>{var r=n(90953),o=n(31136),s=n(49677),i=n(65988);e.exports=function(e,t,n){for(var a=o(t),l=i.f,c=s.f,u=0;u{var r=n(99813)("match");e.exports=function(e){var t=/./;try{"/./"[e](t)}catch(n){try{return t[r]=!1,"/./"[e](t)}catch(e){}}return!1}},64160:(e,t,n)=>{var r=n(95981);e.exports=!r((function(){function e(){}return e.prototype.constructor=null,Object.getPrototypeOf(new e)!==e.prototype}))},23538:e=>{e.exports=function(e,t){return{value:e,done:t}}},32029:(e,t,n)=>{var r=n(55746),o=n(65988),s=n(31887);e.exports=r?function(e,t,n){return o.f(e,t,s(1,n))}:function(e,t,n){return e[t]=n,e}},31887:e=>{e.exports=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}}},55449:(e,t,n)=>{"use strict";var r=n(83894),o=n(65988),s=n(31887);e.exports=function(e,t,n){var i=r(t);i in e?o.f(e,i,s(0,n)):e[i]=n}},29202:(e,t,n)=>{var r=n(65988);e.exports=function(e,t,n){return r.f(e,t,n)}},95929:(e,t,n)=>{var r=n(32029);e.exports=function(e,t,n,o){return o&&o.enumerable?e[t]=n:r(e,t,n),e}},94380:(e,t,n)=>{var r=n(95929);e.exports=function(e,t,n){for(var o in t)n&&n.unsafe&&e[o]?e[o]=t[o]:r(e,o,t[o],n);return e}},75609:(e,t,n)=>{var r=n(21899),o=Object.defineProperty;e.exports=function(e,t){try{o(r,e,{value:t,configurable:!0,writable:!0})}catch(n){r[e]=t}return t}},15863:(e,t,n)=>{"use strict";var r=n(69826),o=TypeError;e.exports=function(e,t){if(!delete e[t])throw o("Cannot delete property "+r(t)+" of "+r(e))}},55746:(e,t,n)=>{var r=n(95981);e.exports=!r((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]}))},76616:e=>{var t="object"==typeof document&&document.all,n=void 0===t&&void 0!==t;e.exports={all:t,IS_HTMLDDA:n}},61333:(e,t,n)=>{var r=n(21899),o=n(10941),s=r.document,i=o(s)&&o(s.createElement);e.exports=function(e){return i?s.createElement(e):{}}},66796:e=>{var t=TypeError;e.exports=function(e){if(e>9007199254740991)throw t("Maximum allowed index exceeded");return e}},63281:e=>{e.exports={CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0}},34342:(e,t,n)=>{var r=n(2861).match(/firefox\/(\d+)/i);e.exports=!!r&&+r[1]},23321:(e,t,n)=>{var r=n(48501),o=n(6049);e.exports=!r&&!o&&"object"==typeof window&&"object"==typeof document},56491:e=>{e.exports="function"==typeof Bun&&Bun&&"string"==typeof Bun.version},48501:e=>{e.exports="object"==typeof Deno&&Deno&&"object"==typeof Deno.version},81046:(e,t,n)=>{var r=n(2861);e.exports=/MSIE|Trident/.test(r)},4470:(e,t,n)=>{var r=n(2861);e.exports=/ipad|iphone|ipod/i.test(r)&&"undefined"!=typeof Pebble},22749:(e,t,n)=>{var r=n(2861);e.exports=/(?:ipad|iphone|ipod).*applewebkit/i.test(r)},6049:(e,t,n)=>{var r=n(34155),o=n(82532);e.exports=void 0!==r&&"process"==o(r)},58045:(e,t,n)=>{var r=n(2861);e.exports=/web0s(?!.*chrome)/i.test(r)},2861:e=>{e.exports="undefined"!=typeof navigator&&String(navigator.userAgent)||""},53385:(e,t,n)=>{var r,o,s=n(21899),i=n(2861),a=s.process,l=s.Deno,c=a&&a.versions||l&&l.version,u=c&&c.v8;u&&(o=(r=u.split("."))[0]>0&&r[0]<4?1:+(r[0]+r[1])),!o&&i&&(!(r=i.match(/Edge\/(\d+)/))||r[1]>=74)&&(r=i.match(/Chrome\/(\d+)/))&&(o=+r[1]),e.exports=o},18938:(e,t,n)=>{var r=n(2861).match(/AppleWebKit\/(\d+)\./);e.exports=!!r&&+r[1]},35703:(e,t,n)=>{var r=n(54058);e.exports=function(e){return r[e+"Prototype"]}},56759:e=>{e.exports=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"]},53995:(e,t,n)=>{var r=n(95329),o=Error,s=r("".replace),i=String(o("zxcasd").stack),a=/\n\s*at [^:]*:[^\n]*/,l=a.test(i);e.exports=function(e,t){if(l&&"string"==typeof e&&!o.prepareStackTrace)for(;t--;)e=s(e,a,"");return e}},79585:(e,t,n)=>{var r=n(32029),o=n(53995),s=n(18780),i=Error.captureStackTrace;e.exports=function(e,t,n,a){s&&(i?i(e,t):r(e,"stack",o(n,a)))}},18780:(e,t,n)=>{var r=n(95981),o=n(31887);e.exports=!r((function(){var e=Error("a");return!("stack"in e)||(Object.defineProperty(e,"stack",o(1,7)),7!==e.stack)}))},76887:(e,t,n)=>{"use strict";var r=n(21899),o=n(79730),s=n(97484),i=n(57475),a=n(49677).f,l=n(37252),c=n(54058),u=n(86843),p=n(32029),h=n(90953),f=function(e){var t=function(n,r,s){if(this instanceof t){switch(arguments.length){case 0:return new e;case 1:return new e(n);case 2:return new e(n,r)}return new e(n,r,s)}return o(e,this,arguments)};return t.prototype=e.prototype,t};e.exports=function(e,t){var n,o,d,m,g,y,v,b,w,E=e.target,x=e.global,S=e.stat,_=e.proto,j=x?r:S?r[E]:(r[E]||{}).prototype,O=x?c:c[E]||p(c,E,{})[E],k=O.prototype;for(m in t)o=!(n=l(x?m:E+(S?".":"#")+m,e.forced))&&j&&h(j,m),y=O[m],o&&(v=e.dontCallGetSet?(w=a(j,m))&&w.value:j[m]),g=o&&v?v:t[m],o&&typeof y==typeof g||(b=e.bind&&o?u(g,r):e.wrap&&o?f(g):_&&i(g)?s(g):g,(e.sham||g&&g.sham||y&&y.sham)&&p(b,"sham",!0),p(O,m,b),_&&(h(c,d=E+"Prototype")||p(c,d,{}),p(c[d],m,g),e.real&&k&&(n||!k[m])&&p(k,m,g)))}},95981:e=>{e.exports=function(e){try{return!!e()}catch(e){return!0}}},45602:(e,t,n)=>{var r=n(95981);e.exports=!r((function(){return Object.isExtensible(Object.preventExtensions({}))}))},79730:(e,t,n)=>{var r=n(18285),o=Function.prototype,s=o.apply,i=o.call;e.exports="object"==typeof Reflect&&Reflect.apply||(r?i.bind(s):function(){return i.apply(s,arguments)})},86843:(e,t,n)=>{var r=n(97484),o=n(24883),s=n(18285),i=r(r.bind);e.exports=function(e,t){return o(e),void 0===t?e:s?i(e,t):function(){return e.apply(t,arguments)}}},18285:(e,t,n)=>{var r=n(95981);e.exports=!r((function(){var e=function(){}.bind();return"function"!=typeof e||e.hasOwnProperty("prototype")}))},98308:(e,t,n)=>{"use strict";var r=n(95329),o=n(24883),s=n(10941),i=n(90953),a=n(93765),l=n(18285),c=Function,u=r([].concat),p=r([].join),h={};e.exports=l?c.bind:function(e){var t=o(this),n=t.prototype,r=a(arguments,1),l=function(){var n=u(r,a(arguments));return this instanceof l?function(e,t,n){if(!i(h,t)){for(var r=[],o=0;o{var r=n(18285),o=Function.prototype.call;e.exports=r?o.bind(o):function(){return o.apply(o,arguments)}},79417:(e,t,n)=>{var r=n(55746),o=n(90953),s=Function.prototype,i=r&&Object.getOwnPropertyDescriptor,a=o(s,"name"),l=a&&"something"===function(){}.name,c=a&&(!r||r&&i(s,"name").configurable);e.exports={EXISTS:a,PROPER:l,CONFIGURABLE:c}},45526:(e,t,n)=>{var r=n(95329),o=n(24883);e.exports=function(e,t,n){try{return r(o(Object.getOwnPropertyDescriptor(e,t)[n]))}catch(e){}}},97484:(e,t,n)=>{var r=n(82532),o=n(95329);e.exports=function(e){if("Function"===r(e))return o(e)}},95329:(e,t,n)=>{var r=n(18285),o=Function.prototype,s=o.call,i=r&&o.bind.bind(s,s);e.exports=r?i:function(e){return function(){return s.apply(e,arguments)}}},626:(e,t,n)=>{var r=n(54058),o=n(21899),s=n(57475),i=function(e){return s(e)?e:void 0};e.exports=function(e,t){return arguments.length<2?i(r[e])||i(o[e]):r[e]&&r[e][t]||o[e]&&o[e][t]}},22902:(e,t,n)=>{var r=n(9697),o=n(14229),s=n(82119),i=n(12077),a=n(99813)("iterator");e.exports=function(e){if(!s(e))return o(e,a)||o(e,"@@iterator")||i[r(e)]}},53476:(e,t,n)=>{var r=n(78834),o=n(24883),s=n(96059),i=n(69826),a=n(22902),l=TypeError;e.exports=function(e,t){var n=arguments.length<2?a(e):t;if(o(n))return s(r(n,e));throw l(i(e)+" is not iterable")}},33323:(e,t,n)=>{var r=n(95329),o=n(1052),s=n(57475),i=n(82532),a=n(85803),l=r([].push);e.exports=function(e){if(s(e))return e;if(o(e)){for(var t=e.length,n=[],r=0;r{var r=n(24883),o=n(82119);e.exports=function(e,t){var n=e[t];return o(n)?void 0:r(n)}},21899:function(e,t,n){var r=function(e){return e&&e.Math==Math&&e};e.exports=r("object"==typeof globalThis&&globalThis)||r("object"==typeof window&&window)||r("object"==typeof self&&self)||r("object"==typeof n.g&&n.g)||function(){return this}()||this||Function("return this")()},90953:(e,t,n)=>{var r=n(95329),o=n(89678),s=r({}.hasOwnProperty);e.exports=Object.hasOwn||function(e,t){return s(o(e),t)}},27748:e=>{e.exports={}},34845:e=>{e.exports=function(e,t){try{1==arguments.length?console.error(e):console.error(e,t)}catch(e){}}},15463:(e,t,n)=>{var r=n(626);e.exports=r("document","documentElement")},2840:(e,t,n)=>{var r=n(55746),o=n(95981),s=n(61333);e.exports=!r&&!o((function(){return 7!=Object.defineProperty(s("div"),"a",{get:function(){return 7}}).a}))},37026:(e,t,n)=>{var r=n(95329),o=n(95981),s=n(82532),i=Object,a=r("".split);e.exports=o((function(){return!i("z").propertyIsEnumerable(0)}))?function(e){return"String"==s(e)?a(e,""):i(e)}:i},81302:(e,t,n)=>{var r=n(95329),o=n(57475),s=n(63030),i=r(Function.toString);o(s.inspectSource)||(s.inspectSource=function(e){return i(e)}),e.exports=s.inspectSource},53794:(e,t,n)=>{var r=n(10941),o=n(32029);e.exports=function(e,t){r(t)&&"cause"in t&&o(e,"cause",t.cause)}},21647:(e,t,n)=>{var r=n(76887),o=n(95329),s=n(27748),i=n(10941),a=n(90953),l=n(65988).f,c=n(10946),u=n(684),p=n(91584),h=n(99418),f=n(45602),d=!1,m=h("meta"),g=0,y=function(e){l(e,m,{value:{objectID:"O"+g++,weakData:{}}})},v=e.exports={enable:function(){v.enable=function(){},d=!0;var e=c.f,t=o([].splice),n={};n[m]=1,e(n).length&&(c.f=function(n){for(var r=e(n),o=0,s=r.length;o{var r,o,s,i=n(47093),a=n(21899),l=n(10941),c=n(32029),u=n(90953),p=n(63030),h=n(44262),f=n(27748),d="Object already initialized",m=a.TypeError,g=a.WeakMap;if(i||p.state){var y=p.state||(p.state=new g);y.get=y.get,y.has=y.has,y.set=y.set,r=function(e,t){if(y.has(e))throw m(d);return t.facade=e,y.set(e,t),t},o=function(e){return y.get(e)||{}},s=function(e){return y.has(e)}}else{var v=h("state");f[v]=!0,r=function(e,t){if(u(e,v))throw m(d);return t.facade=e,c(e,v,t),t},o=function(e){return u(e,v)?e[v]:{}},s=function(e){return u(e,v)}}e.exports={set:r,get:o,has:s,enforce:function(e){return s(e)?o(e):r(e,{})},getterFor:function(e){return function(t){var n;if(!l(t)||(n=o(t)).type!==e)throw m("Incompatible receiver, "+e+" required");return n}}}},6782:(e,t,n)=>{var r=n(99813),o=n(12077),s=r("iterator"),i=Array.prototype;e.exports=function(e){return void 0!==e&&(o.Array===e||i[s]===e)}},1052:(e,t,n)=>{var r=n(82532);e.exports=Array.isArray||function(e){return"Array"==r(e)}},57475:(e,t,n)=>{var r=n(76616),o=r.all;e.exports=r.IS_HTMLDDA?function(e){return"function"==typeof e||e===o}:function(e){return"function"==typeof e}},24284:(e,t,n)=>{var r=n(95329),o=n(95981),s=n(57475),i=n(9697),a=n(626),l=n(81302),c=function(){},u=[],p=a("Reflect","construct"),h=/^\s*(?:class|function)\b/,f=r(h.exec),d=!h.exec(c),m=function(e){if(!s(e))return!1;try{return p(c,u,e),!0}catch(e){return!1}},g=function(e){if(!s(e))return!1;switch(i(e)){case"AsyncFunction":case"GeneratorFunction":case"AsyncGeneratorFunction":return!1}try{return d||!!f(h,l(e))}catch(e){return!0}};g.sham=!0,e.exports=!p||o((function(){var e;return m(m.call)||!m(Object)||!m((function(){e=!0}))||e}))?g:m},37252:(e,t,n)=>{var r=n(95981),o=n(57475),s=/#|\.prototype\./,i=function(e,t){var n=l[a(e)];return n==u||n!=c&&(o(t)?r(t):!!t)},a=i.normalize=function(e){return String(e).replace(s,".").toLowerCase()},l=i.data={},c=i.NATIVE="N",u=i.POLYFILL="P";e.exports=i},54639:(e,t,n)=>{var r=n(10941),o=Math.floor;e.exports=Number.isInteger||function(e){return!r(e)&&isFinite(e)&&o(e)===e}},82119:e=>{e.exports=function(e){return null==e}},10941:(e,t,n)=>{var r=n(57475),o=n(76616),s=o.all;e.exports=o.IS_HTMLDDA?function(e){return"object"==typeof e?null!==e:r(e)||e===s}:function(e){return"object"==typeof e?null!==e:r(e)}},82529:e=>{e.exports=!0},60685:(e,t,n)=>{var r=n(10941),o=n(82532),s=n(99813)("match");e.exports=function(e){var t;return r(e)&&(void 0!==(t=e[s])?!!t:"RegExp"==o(e))}},56664:(e,t,n)=>{var r=n(626),o=n(57475),s=n(7046),i=n(32302),a=Object;e.exports=i?function(e){return"symbol"==typeof e}:function(e){var t=r("Symbol");return o(t)&&s(t.prototype,a(e))}},93091:(e,t,n)=>{var r=n(86843),o=n(78834),s=n(96059),i=n(69826),a=n(6782),l=n(10623),c=n(7046),u=n(53476),p=n(22902),h=n(7609),f=TypeError,d=function(e,t){this.stopped=e,this.result=t},m=d.prototype;e.exports=function(e,t,n){var g,y,v,b,w,E,x,S=n&&n.that,_=!(!n||!n.AS_ENTRIES),j=!(!n||!n.IS_RECORD),O=!(!n||!n.IS_ITERATOR),k=!(!n||!n.INTERRUPTED),A=r(t,S),C=function(e){return g&&h(g,"normal",e),new d(!0,e)},P=function(e){return _?(s(e),k?A(e[0],e[1],C):A(e[0],e[1])):k?A(e,C):A(e)};if(j)g=e.iterator;else if(O)g=e;else{if(!(y=p(e)))throw f(i(e)+" is not iterable");if(a(y)){for(v=0,b=l(e);b>v;v++)if((w=P(e[v]))&&c(m,w))return w;return new d(!1)}g=u(e,y)}for(E=j?e.next:g.next;!(x=o(E,g)).done;){try{w=P(x.value)}catch(e){h(g,"throw",e)}if("object"==typeof w&&w&&c(m,w))return w}return new d(!1)}},7609:(e,t,n)=>{var r=n(78834),o=n(96059),s=n(14229);e.exports=function(e,t,n){var i,a;o(e);try{if(!(i=s(e,"return"))){if("throw"===t)throw n;return n}i=r(i,e)}catch(e){a=!0,i=e}if("throw"===t)throw n;if(a)throw i;return o(i),n}},53847:(e,t,n)=>{"use strict";var r=n(35143).IteratorPrototype,o=n(29290),s=n(31887),i=n(90904),a=n(12077),l=function(){return this};e.exports=function(e,t,n,c){var u=t+" Iterator";return e.prototype=o(r,{next:s(+!c,n)}),i(e,u,!1,!0),a[u]=l,e}},75105:(e,t,n)=>{"use strict";var r=n(76887),o=n(78834),s=n(82529),i=n(79417),a=n(57475),l=n(53847),c=n(249),u=n(88929),p=n(90904),h=n(32029),f=n(95929),d=n(99813),m=n(12077),g=n(35143),y=i.PROPER,v=i.CONFIGURABLE,b=g.IteratorPrototype,w=g.BUGGY_SAFARI_ITERATORS,E=d("iterator"),x="keys",S="values",_="entries",j=function(){return this};e.exports=function(e,t,n,i,d,g,O){l(n,t,i);var k,A,C,P=function(e){if(e===d&&M)return M;if(!w&&e in T)return T[e];switch(e){case x:case S:case _:return function(){return new n(this,e)}}return function(){return new n(this)}},N=t+" Iterator",I=!1,T=e.prototype,R=T[E]||T["@@iterator"]||d&&T[d],M=!w&&R||P(d),D="Array"==t&&T.entries||R;if(D&&(k=c(D.call(new e)))!==Object.prototype&&k.next&&(s||c(k)===b||(u?u(k,b):a(k[E])||f(k,E,j)),p(k,N,!0,!0),s&&(m[N]=j)),y&&d==S&&R&&R.name!==S&&(!s&&v?h(T,"name",S):(I=!0,M=function(){return o(R,this)})),d)if(A={values:P(S),keys:g?M:P(x),entries:P(_)},O)for(C in A)(w||I||!(C in T))&&f(T,C,A[C]);else r({target:t,proto:!0,forced:w||I},A);return s&&!O||T[E]===M||f(T,E,M,{name:d}),m[t]=M,A}},35143:(e,t,n)=>{"use strict";var r,o,s,i=n(95981),a=n(57475),l=n(10941),c=n(29290),u=n(249),p=n(95929),h=n(99813),f=n(82529),d=h("iterator"),m=!1;[].keys&&("next"in(s=[].keys())?(o=u(u(s)))!==Object.prototype&&(r=o):m=!0),!l(r)||i((function(){var e={};return r[d].call(e)!==e}))?r={}:f&&(r=c(r)),a(r[d])||p(r,d,(function(){return this})),e.exports={IteratorPrototype:r,BUGGY_SAFARI_ITERATORS:m}},12077:e=>{e.exports={}},10623:(e,t,n)=>{var r=n(43057);e.exports=function(e){return r(e.length)}},35331:e=>{var t=Math.ceil,n=Math.floor;e.exports=Math.trunc||function(e){var r=+e;return(r>0?n:t)(r)}},66132:(e,t,n)=>{var r,o,s,i,a,l=n(21899),c=n(86843),u=n(49677).f,p=n(42941).set,h=n(18397),f=n(22749),d=n(4470),m=n(58045),g=n(6049),y=l.MutationObserver||l.WebKitMutationObserver,v=l.document,b=l.process,w=l.Promise,E=u(l,"queueMicrotask"),x=E&&E.value;if(!x){var S=new h,_=function(){var e,t;for(g&&(e=b.domain)&&e.exit();t=S.get();)try{t()}catch(e){throw S.head&&r(),e}e&&e.enter()};f||g||m||!y||!v?!d&&w&&w.resolve?((i=w.resolve(void 0)).constructor=w,a=c(i.then,i),r=function(){a(_)}):g?r=function(){b.nextTick(_)}:(p=c(p,l),r=function(){p(_)}):(o=!0,s=v.createTextNode(""),new y(_).observe(s,{characterData:!0}),r=function(){s.data=o=!o}),x=function(e){S.head||r(),S.add(e)}}e.exports=x},69520:(e,t,n)=>{"use strict";var r=n(24883),o=TypeError,s=function(e){var t,n;this.promise=new e((function(e,r){if(void 0!==t||void 0!==n)throw o("Bad Promise constructor");t=e,n=r})),this.resolve=r(t),this.reject=r(n)};e.exports.f=function(e){return new s(e)}},14649:(e,t,n)=>{var r=n(85803);e.exports=function(e,t){return void 0===e?arguments.length<2?"":t:r(e)}},70344:(e,t,n)=>{var r=n(60685),o=TypeError;e.exports=function(e){if(r(e))throw o("The method doesn't accept regular expressions");return e}},24420:(e,t,n)=>{"use strict";var r=n(55746),o=n(95329),s=n(78834),i=n(95981),a=n(14771),l=n(87857),c=n(36760),u=n(89678),p=n(37026),h=Object.assign,f=Object.defineProperty,d=o([].concat);e.exports=!h||i((function(){if(r&&1!==h({b:1},h(f({},"a",{enumerable:!0,get:function(){f(this,"b",{value:3,enumerable:!1})}}),{b:2})).b)return!0;var e={},t={},n=Symbol(),o="abcdefghijklmnopqrst";return e[n]=7,o.split("").forEach((function(e){t[e]=e})),7!=h({},e)[n]||a(h({},t)).join("")!=o}))?function(e,t){for(var n=u(e),o=arguments.length,i=1,h=l.f,f=c.f;o>i;)for(var m,g=p(arguments[i++]),y=h?d(a(g),h(g)):a(g),v=y.length,b=0;v>b;)m=y[b++],r&&!s(f,g,m)||(n[m]=g[m]);return n}:h},29290:(e,t,n)=>{var r,o=n(96059),s=n(59938),i=n(56759),a=n(27748),l=n(15463),c=n(61333),u=n(44262),p="prototype",h="script",f=u("IE_PROTO"),d=function(){},m=function(e){return"<"+h+">"+e+""},g=function(e){e.write(m("")),e.close();var t=e.parentWindow.Object;return e=null,t},y=function(){try{r=new ActiveXObject("htmlfile")}catch(e){}var e,t,n;y="undefined"!=typeof document?document.domain&&r?g(r):(t=c("iframe"),n="java"+h+":",t.style.display="none",l.appendChild(t),t.src=String(n),(e=t.contentWindow.document).open(),e.write(m("document.F=Object")),e.close(),e.F):g(r);for(var o=i.length;o--;)delete y[p][i[o]];return y()};a[f]=!0,e.exports=Object.create||function(e,t){var n;return null!==e?(d[p]=o(e),n=new d,d[p]=null,n[f]=e):n=y(),void 0===t?n:s.f(n,t)}},59938:(e,t,n)=>{var r=n(55746),o=n(83937),s=n(65988),i=n(96059),a=n(74529),l=n(14771);t.f=r&&!o?Object.defineProperties:function(e,t){i(e);for(var n,r=a(t),o=l(t),c=o.length,u=0;c>u;)s.f(e,n=o[u++],r[n]);return e}},65988:(e,t,n)=>{var r=n(55746),o=n(2840),s=n(83937),i=n(96059),a=n(83894),l=TypeError,c=Object.defineProperty,u=Object.getOwnPropertyDescriptor,p="enumerable",h="configurable",f="writable";t.f=r?s?function(e,t,n){if(i(e),t=a(t),i(n),"function"==typeof e&&"prototype"===t&&"value"in n&&f in n&&!n[f]){var r=u(e,t);r&&r[f]&&(e[t]=n.value,n={configurable:h in n?n[h]:r[h],enumerable:p in n?n[p]:r[p],writable:!1})}return c(e,t,n)}:c:function(e,t,n){if(i(e),t=a(t),i(n),o)try{return c(e,t,n)}catch(e){}if("get"in n||"set"in n)throw l("Accessors not supported");return"value"in n&&(e[t]=n.value),e}},49677:(e,t,n)=>{var r=n(55746),o=n(78834),s=n(36760),i=n(31887),a=n(74529),l=n(83894),c=n(90953),u=n(2840),p=Object.getOwnPropertyDescriptor;t.f=r?p:function(e,t){if(e=a(e),t=l(t),u)try{return p(e,t)}catch(e){}if(c(e,t))return i(!o(s.f,e,t),e[t])}},684:(e,t,n)=>{var r=n(82532),o=n(74529),s=n(10946).f,i=n(15790),a="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[];e.exports.f=function(e){return a&&"Window"==r(e)?function(e){try{return s(e)}catch(e){return i(a)}}(e):s(o(e))}},10946:(e,t,n)=>{var r=n(55629),o=n(56759).concat("length","prototype");t.f=Object.getOwnPropertyNames||function(e){return r(e,o)}},87857:(e,t)=>{t.f=Object.getOwnPropertySymbols},249:(e,t,n)=>{var r=n(90953),o=n(57475),s=n(89678),i=n(44262),a=n(64160),l=i("IE_PROTO"),c=Object,u=c.prototype;e.exports=a?c.getPrototypeOf:function(e){var t=s(e);if(r(t,l))return t[l];var n=t.constructor;return o(n)&&t instanceof n?n.prototype:t instanceof c?u:null}},91584:(e,t,n)=>{var r=n(95981),o=n(10941),s=n(82532),i=n(97135),a=Object.isExtensible,l=r((function(){a(1)}));e.exports=l||i?function(e){return!!o(e)&&((!i||"ArrayBuffer"!=s(e))&&(!a||a(e)))}:a},7046:(e,t,n)=>{var r=n(95329);e.exports=r({}.isPrototypeOf)},55629:(e,t,n)=>{var r=n(95329),o=n(90953),s=n(74529),i=n(31692).indexOf,a=n(27748),l=r([].push);e.exports=function(e,t){var n,r=s(e),c=0,u=[];for(n in r)!o(a,n)&&o(r,n)&&l(u,n);for(;t.length>c;)o(r,n=t[c++])&&(~i(u,n)||l(u,n));return u}},14771:(e,t,n)=>{var r=n(55629),o=n(56759);e.exports=Object.keys||function(e){return r(e,o)}},36760:(e,t)=>{"use strict";var n={}.propertyIsEnumerable,r=Object.getOwnPropertyDescriptor,o=r&&!n.call({1:2},1);t.f=o?function(e){var t=r(this,e);return!!t&&t.enumerable}:n},88929:(e,t,n)=>{var r=n(45526),o=n(96059),s=n(11851);e.exports=Object.setPrototypeOf||("__proto__"in{}?function(){var e,t=!1,n={};try{(e=r(Object.prototype,"__proto__","set"))(n,[]),t=n instanceof Array}catch(e){}return function(n,r){return o(n),s(r),t?e(n,r):n.__proto__=r,n}}():void 0)},88810:(e,t,n)=>{var r=n(55746),o=n(95981),s=n(95329),i=n(249),a=n(14771),l=n(74529),c=s(n(36760).f),u=s([].push),p=r&&o((function(){var e=Object.create(null);return e[2]=2,!c(e,2)})),h=function(e){return function(t){for(var n,o=l(t),s=a(o),h=p&&null===i(o),f=s.length,d=0,m=[];f>d;)n=s[d++],r&&!(h?n in o:c(o,n))||u(m,e?[n,o[n]]:o[n]);return m}};e.exports={entries:h(!0),values:h(!1)}},95623:(e,t,n)=>{"use strict";var r=n(22885),o=n(9697);e.exports=r?{}.toString:function(){return"[object "+o(this)+"]"}},39811:(e,t,n)=>{var r=n(78834),o=n(57475),s=n(10941),i=TypeError;e.exports=function(e,t){var n,a;if("string"===t&&o(n=e.toString)&&!s(a=r(n,e)))return a;if(o(n=e.valueOf)&&!s(a=r(n,e)))return a;if("string"!==t&&o(n=e.toString)&&!s(a=r(n,e)))return a;throw i("Can't convert object to primitive value")}},31136:(e,t,n)=>{var r=n(626),o=n(95329),s=n(10946),i=n(87857),a=n(96059),l=o([].concat);e.exports=r("Reflect","ownKeys")||function(e){var t=s.f(a(e)),n=i.f;return n?l(t,n(e)):t}},54058:e=>{e.exports={}},40002:e=>{e.exports=function(e){try{return{error:!1,value:e()}}catch(e){return{error:!0,value:e}}}},67742:(e,t,n)=>{var r=n(21899),o=n(6991),s=n(57475),i=n(37252),a=n(81302),l=n(99813),c=n(23321),u=n(48501),p=n(82529),h=n(53385),f=o&&o.prototype,d=l("species"),m=!1,g=s(r.PromiseRejectionEvent),y=i("Promise",(function(){var e=a(o),t=e!==String(o);if(!t&&66===h)return!0;if(p&&(!f.catch||!f.finally))return!0;if(!h||h<51||!/native code/.test(e)){var n=new o((function(e){e(1)})),r=function(e){e((function(){}),(function(){}))};if((n.constructor={})[d]=r,!(m=n.then((function(){}))instanceof r))return!0}return!t&&(c||u)&&!g}));e.exports={CONSTRUCTOR:y,REJECTION_EVENT:g,SUBCLASSING:m}},6991:(e,t,n)=>{var r=n(21899);e.exports=r.Promise},56584:(e,t,n)=>{var r=n(96059),o=n(10941),s=n(69520);e.exports=function(e,t){if(r(e),o(t)&&t.constructor===e)return t;var n=s.f(e);return(0,n.resolve)(t),n.promise}},31542:(e,t,n)=>{var r=n(6991),o=n(21385),s=n(67742).CONSTRUCTOR;e.exports=s||!o((function(e){r.all(e).then(void 0,(function(){}))}))},18397:e=>{var t=function(){this.head=null,this.tail=null};t.prototype={add:function(e){var t={item:e,next:null},n=this.tail;n?n.next=t:this.head=t,this.tail=t},get:function(){var e=this.head;if(e)return null===(this.head=e.next)&&(this.tail=null),e.item}},e.exports=t},48219:(e,t,n)=>{var r=n(82119),o=TypeError;e.exports=function(e){if(r(e))throw o("Can't call method on "+e);return e}},37620:(e,t,n)=>{"use strict";var r,o=n(21899),s=n(79730),i=n(57475),a=n(56491),l=n(2861),c=n(93765),u=n(18348),p=o.Function,h=/MSIE .\./.test(l)||a&&((r=o.Bun.version.split(".")).length<3||0==r[0]&&(r[1]<3||3==r[1]&&0==r[2]));e.exports=function(e,t){var n=t?2:1;return h?function(r,o){var a=u(arguments.length,1)>n,l=i(r)?r:p(r),h=a?c(arguments,n):[],f=a?function(){s(l,this,h)}:l;return t?e(f,o):e(f)}:e}},94431:(e,t,n)=>{"use strict";var r=n(626),o=n(29202),s=n(99813),i=n(55746),a=s("species");e.exports=function(e){var t=r(e);i&&t&&!t[a]&&o(t,a,{configurable:!0,get:function(){return this}})}},90904:(e,t,n)=>{var r=n(22885),o=n(65988).f,s=n(32029),i=n(90953),a=n(95623),l=n(99813)("toStringTag");e.exports=function(e,t,n,c){if(e){var u=n?e:e.prototype;i(u,l)||o(u,l,{configurable:!0,value:t}),c&&!r&&s(u,"toString",a)}}},44262:(e,t,n)=>{var r=n(68726),o=n(99418),s=r("keys");e.exports=function(e){return s[e]||(s[e]=o(e))}},63030:(e,t,n)=>{var r=n(21899),o=n(75609),s="__core-js_shared__",i=r[s]||o(s,{});e.exports=i},68726:(e,t,n)=>{var r=n(82529),o=n(63030);(e.exports=function(e,t){return o[e]||(o[e]=void 0!==t?t:{})})("versions",[]).push({version:"3.31.0",mode:r?"pure":"global",copyright:"© 2014-2023 Denis Pushkarev (zloirock.ru)",license:"https://github.com/zloirock/core-js/blob/v3.31.0/LICENSE",source:"https://github.com/zloirock/core-js"})},70487:(e,t,n)=>{var r=n(96059),o=n(174),s=n(82119),i=n(99813)("species");e.exports=function(e,t){var n,a=r(e).constructor;return void 0===a||s(n=r(a)[i])?t:o(n)}},64620:(e,t,n)=>{var r=n(95329),o=n(62435),s=n(85803),i=n(48219),a=r("".charAt),l=r("".charCodeAt),c=r("".slice),u=function(e){return function(t,n){var r,u,p=s(i(t)),h=o(n),f=p.length;return h<0||h>=f?e?"":void 0:(r=l(p,h))<55296||r>56319||h+1===f||(u=l(p,h+1))<56320||u>57343?e?a(p,h):r:e?c(p,h,h+2):u-56320+(r-55296<<10)+65536}};e.exports={codeAt:u(!1),charAt:u(!0)}},73291:(e,t,n)=>{var r=n(95329),o=2147483647,s=/[^\0-\u007E]/,i=/[.\u3002\uFF0E\uFF61]/g,a="Overflow: input needs wider integers to process",l=RangeError,c=r(i.exec),u=Math.floor,p=String.fromCharCode,h=r("".charCodeAt),f=r([].join),d=r([].push),m=r("".replace),g=r("".split),y=r("".toLowerCase),v=function(e){return e+22+75*(e<26)},b=function(e,t,n){var r=0;for(e=n?u(e/700):e>>1,e+=u(e/t);e>455;)e=u(e/35),r+=36;return u(r+36*e/(e+38))},w=function(e){var t=[];e=function(e){for(var t=[],n=0,r=e.length;n=55296&&o<=56319&&n=i&&ru((o-c)/E))throw l(a);for(c+=(w-i)*E,i=w,n=0;no)throw l(a);if(r==i){for(var x=c,S=36;;){var _=S<=m?1:S>=m+26?26:S-m;if(x<_)break;var j=x-_,O=36-_;d(t,p(v(_+j%O))),x=u(j/O),S+=36}d(t,p(v(x))),m=b(c,E,y==g),c=0,y++}}c++,i++}return f(t,"")};e.exports=function(e){var t,n,r=[],o=g(m(y(e),i,"."),".");for(t=0;t{"use strict";var r=n(62435),o=n(85803),s=n(48219),i=RangeError;e.exports=function(e){var t=o(s(this)),n="",a=r(e);if(a<0||a==1/0)throw i("Wrong number of repetitions");for(;a>0;(a>>>=1)&&(t+=t))1&a&&(n+=t);return n}},93093:(e,t,n)=>{var r=n(79417).PROPER,o=n(95981),s=n(73483);e.exports=function(e){return o((function(){return!!s[e]()||"​…᠎"!=="​…᠎"[e]()||r&&s[e].name!==e}))}},74853:(e,t,n)=>{var r=n(95329),o=n(48219),s=n(85803),i=n(73483),a=r("".replace),l=RegExp("^["+i+"]+"),c=RegExp("(^|[^"+i+"])["+i+"]+$"),u=function(e){return function(t){var n=s(o(t));return 1&e&&(n=a(n,l,"")),2&e&&(n=a(n,c,"$1")),n}};e.exports={start:u(1),end:u(2),trim:u(3)}},63405:(e,t,n)=>{var r=n(53385),o=n(95981),s=n(21899).String;e.exports=!!Object.getOwnPropertySymbols&&!o((function(){var e=Symbol();return!s(e)||!(Object(e)instanceof Symbol)||!Symbol.sham&&r&&r<41}))},29630:(e,t,n)=>{var r=n(78834),o=n(626),s=n(99813),i=n(95929);e.exports=function(){var e=o("Symbol"),t=e&&e.prototype,n=t&&t.valueOf,a=s("toPrimitive");t&&!t[a]&&i(t,a,(function(e){return r(n,this)}),{arity:1})}},32087:(e,t,n)=>{var r=n(626),o=n(95329),s=r("Symbol"),i=s.keyFor,a=o(s.prototype.valueOf);e.exports=s.isRegisteredSymbol||function(e){try{return void 0!==i(a(e))}catch(e){return!1}}},96559:(e,t,n)=>{for(var r=n(68726),o=n(626),s=n(95329),i=n(56664),a=n(99813),l=o("Symbol"),c=l.isWellKnownSymbol,u=o("Object","getOwnPropertyNames"),p=s(l.prototype.valueOf),h=r("wks"),f=0,d=u(l),m=d.length;f{var r=n(63405);e.exports=r&&!!Symbol.for&&!!Symbol.keyFor},42941:(e,t,n)=>{var r,o,s,i,a=n(21899),l=n(79730),c=n(86843),u=n(57475),p=n(90953),h=n(95981),f=n(15463),d=n(93765),m=n(61333),g=n(18348),y=n(22749),v=n(6049),b=a.setImmediate,w=a.clearImmediate,E=a.process,x=a.Dispatch,S=a.Function,_=a.MessageChannel,j=a.String,O=0,k={},A="onreadystatechange";h((function(){r=a.location}));var C=function(e){if(p(k,e)){var t=k[e];delete k[e],t()}},P=function(e){return function(){C(e)}},N=function(e){C(e.data)},I=function(e){a.postMessage(j(e),r.protocol+"//"+r.host)};b&&w||(b=function(e){g(arguments.length,1);var t=u(e)?e:S(e),n=d(arguments,1);return k[++O]=function(){l(t,void 0,n)},o(O),O},w=function(e){delete k[e]},v?o=function(e){E.nextTick(P(e))}:x&&x.now?o=function(e){x.now(P(e))}:_&&!y?(i=(s=new _).port2,s.port1.onmessage=N,o=c(i.postMessage,i)):a.addEventListener&&u(a.postMessage)&&!a.importScripts&&r&&"file:"!==r.protocol&&!h(I)?(o=I,a.addEventListener("message",N,!1)):o=A in m("script")?function(e){f.appendChild(m("script"))[A]=function(){f.removeChild(this),C(e)}}:function(e){setTimeout(P(e),0)}),e.exports={set:b,clear:w}},59413:(e,t,n)=>{var r=n(62435),o=Math.max,s=Math.min;e.exports=function(e,t){var n=r(e);return n<0?o(n+t,0):s(n,t)}},74529:(e,t,n)=>{var r=n(37026),o=n(48219);e.exports=function(e){return r(o(e))}},62435:(e,t,n)=>{var r=n(35331);e.exports=function(e){var t=+e;return t!=t||0===t?0:r(t)}},43057:(e,t,n)=>{var r=n(62435),o=Math.min;e.exports=function(e){return e>0?o(r(e),9007199254740991):0}},89678:(e,t,n)=>{var r=n(48219),o=Object;e.exports=function(e){return o(r(e))}},46935:(e,t,n)=>{var r=n(78834),o=n(10941),s=n(56664),i=n(14229),a=n(39811),l=n(99813),c=TypeError,u=l("toPrimitive");e.exports=function(e,t){if(!o(e)||s(e))return e;var n,l=i(e,u);if(l){if(void 0===t&&(t="default"),n=r(l,e,t),!o(n)||s(n))return n;throw c("Can't convert object to primitive value")}return void 0===t&&(t="number"),a(e,t)}},83894:(e,t,n)=>{var r=n(46935),o=n(56664);e.exports=function(e){var t=r(e,"string");return o(t)?t:t+""}},22885:(e,t,n)=>{var r={};r[n(99813)("toStringTag")]="z",e.exports="[object z]"===String(r)},85803:(e,t,n)=>{var r=n(9697),o=String;e.exports=function(e){if("Symbol"===r(e))throw TypeError("Cannot convert a Symbol value to a string");return o(e)}},69826:e=>{var t=String;e.exports=function(e){try{return t(e)}catch(e){return"Object"}}},99418:(e,t,n)=>{var r=n(95329),o=0,s=Math.random(),i=r(1..toString);e.exports=function(e){return"Symbol("+(void 0===e?"":e)+")_"+i(++o+s,36)}},14766:(e,t,n)=>{var r=n(95981),o=n(99813),s=n(55746),i=n(82529),a=o("iterator");e.exports=!r((function(){var e=new URL("b?a=1&b=2&c=3","http://a"),t=e.searchParams,n=new URLSearchParams("a=1&a=2"),r="";return e.pathname="c%20d",t.forEach((function(e,n){t.delete("b"),r+=n+e})),n.delete("a",2),i&&(!e.toJSON||!n.has("a",1)||n.has("a",2))||!t.size&&(i||!s)||!t.sort||"http://a/c%20d?a=1&c=3"!==e.href||"3"!==t.get("c")||"a=1"!==String(new URLSearchParams("?a=1"))||!t[a]||"a"!==new URL("https://a@b").username||"b"!==new URLSearchParams(new URLSearchParams("a=b")).get("a")||"xn--e1aybc"!==new URL("http://теÑÑ‚").host||"#%D0%B1"!==new URL("http://a#б").hash||"a1c3"!==r||"x"!==new URL("http://x",void 0).host}))},32302:(e,t,n)=>{var r=n(63405);e.exports=r&&!Symbol.sham&&"symbol"==typeof Symbol.iterator},83937:(e,t,n)=>{var r=n(55746),o=n(95981);e.exports=r&&o((function(){return 42!=Object.defineProperty((function(){}),"prototype",{value:42,writable:!1}).prototype}))},18348:e=>{var t=TypeError;e.exports=function(e,n){if(e{var r=n(21899),o=n(57475),s=r.WeakMap;e.exports=o(s)&&/native code/.test(String(s))},73464:(e,t,n)=>{var r=n(54058),o=n(90953),s=n(11477),i=n(65988).f;e.exports=function(e){var t=r.Symbol||(r.Symbol={});o(t,e)||i(t,e,{value:s.f(e)})}},11477:(e,t,n)=>{var r=n(99813);t.f=r},99813:(e,t,n)=>{var r=n(21899),o=n(68726),s=n(90953),i=n(99418),a=n(63405),l=n(32302),c=r.Symbol,u=o("wks"),p=l?c.for||c:c&&c.withoutSetter||i;e.exports=function(e){return s(u,e)||(u[e]=a&&s(c,e)?c[e]:p("Symbol."+e)),u[e]}},73483:e=>{e.exports="\t\n\v\f\r    â€â€‚         âŸã€€\u2028\u2029\ufeff"},49812:(e,t,n)=>{"use strict";var r=n(76887),o=n(7046),s=n(249),i=n(88929),a=n(23489),l=n(29290),c=n(32029),u=n(31887),p=n(53794),h=n(79585),f=n(93091),d=n(14649),m=n(99813)("toStringTag"),g=Error,y=[].push,v=function(e,t){var n,r=o(b,this);i?n=i(g(),r?s(this):b):(n=r?this:l(b),c(n,m,"Error")),void 0!==t&&c(n,"message",d(t)),h(n,v,n.stack,1),arguments.length>2&&p(n,arguments[2]);var a=[];return f(e,y,{that:a}),c(n,"errors",a),n};i?i(v,g):a(v,g,{name:!0});var b=v.prototype=l(g.prototype,{constructor:u(1,v),message:u(1,""),name:u(1,"AggregateError")});r({global:!0,constructor:!0,arity:2},{AggregateError:v})},47627:(e,t,n)=>{n(49812)},85906:(e,t,n)=>{"use strict";var r=n(76887),o=n(95981),s=n(1052),i=n(10941),a=n(89678),l=n(10623),c=n(66796),u=n(55449),p=n(64692),h=n(50568),f=n(99813),d=n(53385),m=f("isConcatSpreadable"),g=d>=51||!o((function(){var e=[];return e[m]=!1,e.concat()[0]!==e})),y=function(e){if(!i(e))return!1;var t=e[m];return void 0!==t?!!t:s(e)};r({target:"Array",proto:!0,arity:1,forced:!g||!h("concat")},{concat:function(e){var t,n,r,o,s,i=a(this),h=p(i,0),f=0;for(t=-1,r=arguments.length;t{"use strict";var r=n(76887),o=n(3610).every;r({target:"Array",proto:!0,forced:!n(34194)("every")},{every:function(e){return o(this,e,arguments.length>1?arguments[1]:void 0)}})},80290:(e,t,n)=>{var r=n(76887),o=n(91860),s=n(18479);r({target:"Array",proto:!0},{fill:o}),s("fill")},21501:(e,t,n)=>{"use strict";var r=n(76887),o=n(3610).filter;r({target:"Array",proto:!0,forced:!n(50568)("filter")},{filter:function(e){return o(this,e,arguments.length>1?arguments[1]:void 0)}})},44929:(e,t,n)=>{"use strict";var r=n(76887),o=n(3610).findIndex,s=n(18479),i="findIndex",a=!0;i in[]&&Array(1)[i]((function(){a=!1})),r({target:"Array",proto:!0,forced:a},{findIndex:function(e){return o(this,e,arguments.length>1?arguments[1]:void 0)}}),s(i)},80833:(e,t,n)=>{"use strict";var r=n(76887),o=n(3610).find,s=n(18479),i="find",a=!0;i in[]&&Array(1)[i]((function(){a=!1})),r({target:"Array",proto:!0,forced:a},{find:function(e){return o(this,e,arguments.length>1?arguments[1]:void 0)}}),s(i)},2437:(e,t,n)=>{"use strict";var r=n(76887),o=n(56837);r({target:"Array",proto:!0,forced:[].forEach!=o},{forEach:o})},53242:(e,t,n)=>{var r=n(76887),o=n(11354);r({target:"Array",stat:!0,forced:!n(21385)((function(e){Array.from(e)}))},{from:o})},97690:(e,t,n)=>{"use strict";var r=n(76887),o=n(31692).includes,s=n(95981),i=n(18479);r({target:"Array",proto:!0,forced:s((function(){return!Array(1).includes()}))},{includes:function(e){return o(this,e,arguments.length>1?arguments[1]:void 0)}}),i("includes")},99076:(e,t,n)=>{"use strict";var r=n(76887),o=n(97484),s=n(31692).indexOf,i=n(34194),a=o([].indexOf),l=!!a&&1/a([1],1,-0)<0;r({target:"Array",proto:!0,forced:l||!i("indexOf")},{indexOf:function(e){var t=arguments.length>1?arguments[1]:void 0;return l?a(this,e,t)||0:s(this,e,t)}})},92737:(e,t,n)=>{n(76887)({target:"Array",stat:!0},{isArray:n(1052)})},66274:(e,t,n)=>{"use strict";var r=n(74529),o=n(18479),s=n(12077),i=n(45402),a=n(65988).f,l=n(75105),c=n(23538),u=n(82529),p=n(55746),h="Array Iterator",f=i.set,d=i.getterFor(h);e.exports=l(Array,"Array",(function(e,t){f(this,{type:h,target:r(e),index:0,kind:t})}),(function(){var e=d(this),t=e.target,n=e.kind,r=e.index++;return!t||r>=t.length?(e.target=void 0,c(void 0,!0)):c("keys"==n?r:"values"==n?t[r]:[r,t[r]],!1)}),"values");var m=s.Arguments=s.Array;if(o("keys"),o("values"),o("entries"),!u&&p&&"values"!==m.name)try{a(m,"name",{value:"values"})}catch(e){}},75915:(e,t,n)=>{var r=n(76887),o=n(67145);r({target:"Array",proto:!0,forced:o!==[].lastIndexOf},{lastIndexOf:o})},68787:(e,t,n)=>{"use strict";var r=n(76887),o=n(3610).map;r({target:"Array",proto:!0,forced:!n(50568)("map")},{map:function(e){return o(this,e,arguments.length>1?arguments[1]:void 0)}})},48528:(e,t,n)=>{"use strict";var r=n(76887),o=n(89678),s=n(10623),i=n(89779),a=n(66796);r({target:"Array",proto:!0,arity:1,forced:n(95981)((function(){return 4294967297!==[].push.call({length:4294967296},1)}))||!function(){try{Object.defineProperty([],"length",{writable:!1}).push()}catch(e){return e instanceof TypeError}}()},{push:function(e){var t=o(this),n=s(t),r=arguments.length;a(n+r);for(var l=0;l{"use strict";var r=n(76887),o=n(46499).left,s=n(34194),i=n(53385);r({target:"Array",proto:!0,forced:!n(6049)&&i>79&&i<83||!s("reduce")},{reduce:function(e){var t=arguments.length;return o(this,e,t,t>1?arguments[1]:void 0)}})},60186:(e,t,n)=>{"use strict";var r=n(76887),o=n(1052),s=n(24284),i=n(10941),a=n(59413),l=n(10623),c=n(74529),u=n(55449),p=n(99813),h=n(50568),f=n(93765),d=h("slice"),m=p("species"),g=Array,y=Math.max;r({target:"Array",proto:!0,forced:!d},{slice:function(e,t){var n,r,p,h=c(this),d=l(h),v=a(e,d),b=a(void 0===t?d:t,d);if(o(h)&&(n=h.constructor,(s(n)&&(n===g||o(n.prototype))||i(n)&&null===(n=n[m]))&&(n=void 0),n===g||void 0===n))return f(h,v,b);for(r=new(void 0===n?g:n)(y(b-v,0)),p=0;v{"use strict";var r=n(76887),o=n(3610).some;r({target:"Array",proto:!0,forced:!n(34194)("some")},{some:function(e){return o(this,e,arguments.length>1?arguments[1]:void 0)}})},4115:(e,t,n)=>{"use strict";var r=n(76887),o=n(95329),s=n(24883),i=n(89678),a=n(10623),l=n(15863),c=n(85803),u=n(95981),p=n(61388),h=n(34194),f=n(34342),d=n(81046),m=n(53385),g=n(18938),y=[],v=o(y.sort),b=o(y.push),w=u((function(){y.sort(void 0)})),E=u((function(){y.sort(null)})),x=h("sort"),S=!u((function(){if(m)return m<70;if(!(f&&f>3)){if(d)return!0;if(g)return g<603;var e,t,n,r,o="";for(e=65;e<76;e++){switch(t=String.fromCharCode(e),e){case 66:case 69:case 70:case 72:n=3;break;case 68:case 71:n=4;break;default:n=2}for(r=0;r<47;r++)y.push({k:t+r,v:n})}for(y.sort((function(e,t){return t.v-e.v})),r=0;rc(n)?1:-1}}(e)),n=a(o),r=0;r{"use strict";var r=n(76887),o=n(89678),s=n(59413),i=n(62435),a=n(10623),l=n(89779),c=n(66796),u=n(64692),p=n(55449),h=n(15863),f=n(50568)("splice"),d=Math.max,m=Math.min;r({target:"Array",proto:!0,forced:!f},{splice:function(e,t){var n,r,f,g,y,v,b=o(this),w=a(b),E=s(e,w),x=arguments.length;for(0===x?n=r=0:1===x?(n=0,r=w-E):(n=x-2,r=m(d(i(t),0),w-E)),c(w+n-r),f=u(b,r),g=0;gw-r+n;g--)h(b,g-1)}else if(n>r)for(g=w-r;g>E;g--)v=g+n-1,(y=g+r-1)in b?b[v]=b[y]:h(b,v);for(g=0;g{var r=n(76887),o=n(95329),s=Date,i=o(s.prototype.getTime);r({target:"Date",stat:!0},{now:function(){return i(new s)}})},18084:()=>{},73381:(e,t,n)=>{var r=n(76887),o=n(98308);r({target:"Function",proto:!0,forced:Function.bind!==o},{bind:o})},32619:(e,t,n)=>{var r=n(76887),o=n(626),s=n(79730),i=n(78834),a=n(95329),l=n(95981),c=n(57475),u=n(56664),p=n(93765),h=n(33323),f=n(63405),d=String,m=o("JSON","stringify"),g=a(/./.exec),y=a("".charAt),v=a("".charCodeAt),b=a("".replace),w=a(1..toString),E=/[\uD800-\uDFFF]/g,x=/^[\uD800-\uDBFF]$/,S=/^[\uDC00-\uDFFF]$/,_=!f||l((function(){var e=o("Symbol")();return"[null]"!=m([e])||"{}"!=m({a:e})||"{}"!=m(Object(e))})),j=l((function(){return'"\\udf06\\ud834"'!==m("\udf06\ud834")||'"\\udead"'!==m("\udead")})),O=function(e,t){var n=p(arguments),r=h(t);if(c(r)||void 0!==e&&!u(e))return n[1]=function(e,t){if(c(r)&&(t=i(r,this,d(e),t)),!u(t))return t},s(m,null,n)},k=function(e,t,n){var r=y(n,t-1),o=y(n,t+1);return g(x,e)&&!g(S,o)||g(S,e)&&!g(x,r)?"\\u"+w(v(e,0),16):e};m&&r({target:"JSON",stat:!0,arity:3,forced:_||j},{stringify:function(e,t,n){var r=p(arguments),o=s(_?O:m,null,r);return j&&"string"==typeof o?b(o,E,k):o}})},69120:(e,t,n)=>{var r=n(21899);n(90904)(r.JSON,"JSON",!0)},23112:(e,t,n)=>{"use strict";n(24683)("Map",(function(e){return function(){return e(this,arguments.length?arguments[0]:void 0)}}),n(85616))},37501:(e,t,n)=>{n(23112)},79413:()=>{},54973:(e,t,n)=>{n(76887)({target:"Number",stat:!0,nonConfigurable:!0,nonWritable:!0},{EPSILON:Math.pow(2,-52)})},30800:(e,t,n)=>{n(76887)({target:"Number",stat:!0},{isInteger:n(54639)})},49221:(e,t,n)=>{var r=n(76887),o=n(24420);r({target:"Object",stat:!0,arity:2,forced:Object.assign!==o},{assign:o})},74979:(e,t,n)=>{var r=n(76887),o=n(55746),s=n(59938).f;r({target:"Object",stat:!0,forced:Object.defineProperties!==s,sham:!o},{defineProperties:s})},86450:(e,t,n)=>{var r=n(76887),o=n(55746),s=n(65988).f;r({target:"Object",stat:!0,forced:Object.defineProperty!==s,sham:!o},{defineProperty:s})},94366:(e,t,n)=>{var r=n(76887),o=n(88810).entries;r({target:"Object",stat:!0},{entries:function(e){return o(e)}})},28387:(e,t,n)=>{var r=n(76887),o=n(93091),s=n(55449);r({target:"Object",stat:!0},{fromEntries:function(e){var t={};return o(e,(function(e,n){s(t,e,n)}),{AS_ENTRIES:!0}),t}})},46924:(e,t,n)=>{var r=n(76887),o=n(95981),s=n(74529),i=n(49677).f,a=n(55746);r({target:"Object",stat:!0,forced:!a||o((function(){i(1)})),sham:!a},{getOwnPropertyDescriptor:function(e,t){return i(s(e),t)}})},88482:(e,t,n)=>{var r=n(76887),o=n(55746),s=n(31136),i=n(74529),a=n(49677),l=n(55449);r({target:"Object",stat:!0,sham:!o},{getOwnPropertyDescriptors:function(e){for(var t,n,r=i(e),o=a.f,c=s(r),u={},p=0;c.length>p;)void 0!==(n=o(r,t=c[p++]))&&l(u,t,n);return u}})},37144:(e,t,n)=>{var r=n(76887),o=n(63405),s=n(95981),i=n(87857),a=n(89678);r({target:"Object",stat:!0,forced:!o||s((function(){i.f(1)}))},{getOwnPropertySymbols:function(e){var t=i.f;return t?t(a(e)):[]}})},21724:(e,t,n)=>{var r=n(76887),o=n(89678),s=n(14771);r({target:"Object",stat:!0,forced:n(95981)((function(){s(1)}))},{keys:function(e){return s(o(e))}})},55967:()=>{},26614:(e,t,n)=>{var r=n(76887),o=n(88810).values;r({target:"Object",stat:!0},{values:function(e){return o(e)}})},4560:(e,t,n)=>{"use strict";var r=n(76887),o=n(78834),s=n(24883),i=n(69520),a=n(40002),l=n(93091);r({target:"Promise",stat:!0,forced:n(31542)},{allSettled:function(e){var t=this,n=i.f(t),r=n.resolve,c=n.reject,u=a((function(){var n=s(t.resolve),i=[],a=0,c=1;l(e,(function(e){var s=a++,l=!1;c++,o(n,t,e).then((function(e){l||(l=!0,i[s]={status:"fulfilled",value:e},--c||r(i))}),(function(e){l||(l=!0,i[s]={status:"rejected",reason:e},--c||r(i))}))})),--c||r(i)}));return u.error&&c(u.value),n.promise}})},16890:(e,t,n)=>{"use strict";var r=n(76887),o=n(78834),s=n(24883),i=n(69520),a=n(40002),l=n(93091);r({target:"Promise",stat:!0,forced:n(31542)},{all:function(e){var t=this,n=i.f(t),r=n.resolve,c=n.reject,u=a((function(){var n=s(t.resolve),i=[],a=0,u=1;l(e,(function(e){var s=a++,l=!1;u++,o(n,t,e).then((function(e){l||(l=!0,i[s]=e,--u||r(i))}),c)})),--u||r(i)}));return u.error&&c(u.value),n.promise}})},91302:(e,t,n)=>{"use strict";var r=n(76887),o=n(78834),s=n(24883),i=n(626),a=n(69520),l=n(40002),c=n(93091),u=n(31542),p="No one promise resolved";r({target:"Promise",stat:!0,forced:u},{any:function(e){var t=this,n=i("AggregateError"),r=a.f(t),u=r.resolve,h=r.reject,f=l((function(){var r=s(t.resolve),i=[],a=0,l=1,f=!1;c(e,(function(e){var s=a++,c=!1;l++,o(r,t,e).then((function(e){c||f||(f=!0,u(e))}),(function(e){c||f||(c=!0,i[s]=e,--l||h(new n(i,p)))}))})),--l||h(new n(i,p))}));return f.error&&h(f.value),r.promise}})},83376:(e,t,n)=>{"use strict";var r=n(76887),o=n(82529),s=n(67742).CONSTRUCTOR,i=n(6991),a=n(626),l=n(57475),c=n(95929),u=i&&i.prototype;if(r({target:"Promise",proto:!0,forced:s,real:!0},{catch:function(e){return this.then(void 0,e)}}),!o&&l(i)){var p=a("Promise").prototype.catch;u.catch!==p&&c(u,"catch",p,{unsafe:!0})}},26934:(e,t,n)=>{"use strict";var r,o,s,i=n(76887),a=n(82529),l=n(6049),c=n(21899),u=n(78834),p=n(95929),h=n(88929),f=n(90904),d=n(94431),m=n(24883),g=n(57475),y=n(10941),v=n(5743),b=n(70487),w=n(42941).set,E=n(66132),x=n(34845),S=n(40002),_=n(18397),j=n(45402),O=n(6991),k=n(67742),A=n(69520),C="Promise",P=k.CONSTRUCTOR,N=k.REJECTION_EVENT,I=k.SUBCLASSING,T=j.getterFor(C),R=j.set,M=O&&O.prototype,D=O,F=M,L=c.TypeError,B=c.document,$=c.process,q=A.f,U=q,z=!!(B&&B.createEvent&&c.dispatchEvent),V="unhandledrejection",W=function(e){var t;return!(!y(e)||!g(t=e.then))&&t},J=function(e,t){var n,r,o,s=t.value,i=1==t.state,a=i?e.ok:e.fail,l=e.resolve,c=e.reject,p=e.domain;try{a?(i||(2===t.rejection&&Y(t),t.rejection=1),!0===a?n=s:(p&&p.enter(),n=a(s),p&&(p.exit(),o=!0)),n===e.promise?c(L("Promise-chain cycle")):(r=W(n))?u(r,n,l,c):l(n)):c(s)}catch(e){p&&!o&&p.exit(),c(e)}},K=function(e,t){e.notified||(e.notified=!0,E((function(){for(var n,r=e.reactions;n=r.get();)J(n,e);e.notified=!1,t&&!e.rejection&&G(e)})))},H=function(e,t,n){var r,o;z?((r=B.createEvent("Event")).promise=t,r.reason=n,r.initEvent(e,!1,!0),c.dispatchEvent(r)):r={promise:t,reason:n},!N&&(o=c["on"+e])?o(r):e===V&&x("Unhandled promise rejection",n)},G=function(e){u(w,c,(function(){var t,n=e.facade,r=e.value;if(Z(e)&&(t=S((function(){l?$.emit("unhandledRejection",r,n):H(V,n,r)})),e.rejection=l||Z(e)?2:1,t.error))throw t.value}))},Z=function(e){return 1!==e.rejection&&!e.parent},Y=function(e){u(w,c,(function(){var t=e.facade;l?$.emit("rejectionHandled",t):H("rejectionhandled",t,e.value)}))},X=function(e,t,n){return function(r){e(t,r,n)}},Q=function(e,t,n){e.done||(e.done=!0,n&&(e=n),e.value=t,e.state=2,K(e,!0))},ee=function(e,t,n){if(!e.done){e.done=!0,n&&(e=n);try{if(e.facade===t)throw L("Promise can't be resolved itself");var r=W(t);r?E((function(){var n={done:!1};try{u(r,t,X(ee,n,e),X(Q,n,e))}catch(t){Q(n,t,e)}})):(e.value=t,e.state=1,K(e,!1))}catch(t){Q({done:!1},t,e)}}};if(P&&(F=(D=function(e){v(this,F),m(e),u(r,this);var t=T(this);try{e(X(ee,t),X(Q,t))}catch(e){Q(t,e)}}).prototype,(r=function(e){R(this,{type:C,done:!1,notified:!1,parent:!1,reactions:new _,rejection:!1,state:0,value:void 0})}).prototype=p(F,"then",(function(e,t){var n=T(this),r=q(b(this,D));return n.parent=!0,r.ok=!g(e)||e,r.fail=g(t)&&t,r.domain=l?$.domain:void 0,0==n.state?n.reactions.add(r):E((function(){J(r,n)})),r.promise})),o=function(){var e=new r,t=T(e);this.promise=e,this.resolve=X(ee,t),this.reject=X(Q,t)},A.f=q=function(e){return e===D||undefined===e?new o(e):U(e)},!a&&g(O)&&M!==Object.prototype)){s=M.then,I||p(M,"then",(function(e,t){var n=this;return new D((function(e,t){u(s,n,e,t)})).then(e,t)}),{unsafe:!0});try{delete M.constructor}catch(e){}h&&h(M,F)}i({global:!0,constructor:!0,wrap:!0,forced:P},{Promise:D}),f(D,C,!1,!0),d(C)},44349:(e,t,n)=>{"use strict";var r=n(76887),o=n(82529),s=n(6991),i=n(95981),a=n(626),l=n(57475),c=n(70487),u=n(56584),p=n(95929),h=s&&s.prototype;if(r({target:"Promise",proto:!0,real:!0,forced:!!s&&i((function(){h.finally.call({then:function(){}},(function(){}))}))},{finally:function(e){var t=c(this,a("Promise")),n=l(e);return this.then(n?function(n){return u(t,e()).then((function(){return n}))}:e,n?function(n){return u(t,e()).then((function(){throw n}))}:e)}}),!o&&l(s)){var f=a("Promise").prototype.finally;h.finally!==f&&p(h,"finally",f,{unsafe:!0})}},98881:(e,t,n)=>{n(26934),n(16890),n(83376),n(55921),n(64069),n(14482)},55921:(e,t,n)=>{"use strict";var r=n(76887),o=n(78834),s=n(24883),i=n(69520),a=n(40002),l=n(93091);r({target:"Promise",stat:!0,forced:n(31542)},{race:function(e){var t=this,n=i.f(t),r=n.reject,c=a((function(){var i=s(t.resolve);l(e,(function(e){o(i,t,e).then(n.resolve,r)}))}));return c.error&&r(c.value),n.promise}})},64069:(e,t,n)=>{"use strict";var r=n(76887),o=n(78834),s=n(69520);r({target:"Promise",stat:!0,forced:n(67742).CONSTRUCTOR},{reject:function(e){var t=s.f(this);return o(t.reject,void 0,e),t.promise}})},14482:(e,t,n)=>{"use strict";var r=n(76887),o=n(626),s=n(82529),i=n(6991),a=n(67742).CONSTRUCTOR,l=n(56584),c=o("Promise"),u=s&&!a;r({target:"Promise",stat:!0,forced:s||a},{resolve:function(e){return l(u&&this===c?i:this,e)}})},1502:()=>{},82266:(e,t,n)=>{"use strict";n(24683)("Set",(function(e){return function(){return e(this,arguments.length?arguments[0]:void 0)}}),n(85616))},69008:(e,t,n)=>{n(82266)},11035:(e,t,n)=>{"use strict";var r=n(76887),o=n(95329),s=n(70344),i=n(48219),a=n(85803),l=n(67772),c=o("".indexOf);r({target:"String",proto:!0,forced:!l("includes")},{includes:function(e){return!!~c(a(i(this)),a(s(e)),arguments.length>1?arguments[1]:void 0)}})},77971:(e,t,n)=>{"use strict";var r=n(64620).charAt,o=n(85803),s=n(45402),i=n(75105),a=n(23538),l="String Iterator",c=s.set,u=s.getterFor(l);i(String,"String",(function(e){c(this,{type:l,string:o(e),index:0})}),(function(){var e,t=u(this),n=t.string,o=t.index;return o>=n.length?a(void 0,!0):(e=r(n,o),t.index+=e.length,a(e,!1))}))},74679:(e,t,n)=>{var r=n(76887),o=n(95329),s=n(74529),i=n(89678),a=n(85803),l=n(10623),c=o([].push),u=o([].join);r({target:"String",stat:!0},{raw:function(e){var t=s(i(e).raw),n=l(t);if(!n)return"";for(var r=arguments.length,o=[],p=0;;){if(c(o,a(t[p++])),p===n)return u(o,"");p{n(76887)({target:"String",proto:!0},{repeat:n(16178)})},94761:(e,t,n)=>{"use strict";var r,o=n(76887),s=n(97484),i=n(49677).f,a=n(43057),l=n(85803),c=n(70344),u=n(48219),p=n(67772),h=n(82529),f=s("".startsWith),d=s("".slice),m=Math.min,g=p("startsWith");o({target:"String",proto:!0,forced:!!(h||g||(r=i(String.prototype,"startsWith"),!r||r.writable))&&!g},{startsWith:function(e){var t=l(u(this));c(e);var n=a(m(arguments.length>1?arguments[1]:void 0,t.length)),r=l(e);return f?f(t,r,n):d(t,n,n+r.length)===r}})},57398:(e,t,n)=>{"use strict";var r=n(76887),o=n(74853).trim;r({target:"String",proto:!0,forced:n(93093)("trim")},{trim:function(){return o(this)}})},8555:(e,t,n)=>{n(73464)("asyncIterator")},48616:(e,t,n)=>{"use strict";var r=n(76887),o=n(21899),s=n(78834),i=n(95329),a=n(82529),l=n(55746),c=n(63405),u=n(95981),p=n(90953),h=n(7046),f=n(96059),d=n(74529),m=n(83894),g=n(85803),y=n(31887),v=n(29290),b=n(14771),w=n(10946),E=n(684),x=n(87857),S=n(49677),_=n(65988),j=n(59938),O=n(36760),k=n(95929),A=n(29202),C=n(68726),P=n(44262),N=n(27748),I=n(99418),T=n(99813),R=n(11477),M=n(73464),D=n(29630),F=n(90904),L=n(45402),B=n(3610).forEach,$=P("hidden"),q="Symbol",U="prototype",z=L.set,V=L.getterFor(q),W=Object[U],J=o.Symbol,K=J&&J[U],H=o.TypeError,G=o.QObject,Z=S.f,Y=_.f,X=E.f,Q=O.f,ee=i([].push),te=C("symbols"),ne=C("op-symbols"),re=C("wks"),oe=!G||!G[U]||!G[U].findChild,se=l&&u((function(){return 7!=v(Y({},"a",{get:function(){return Y(this,"a",{value:7}).a}})).a}))?function(e,t,n){var r=Z(W,t);r&&delete W[t],Y(e,t,n),r&&e!==W&&Y(W,t,r)}:Y,ie=function(e,t){var n=te[e]=v(K);return z(n,{type:q,tag:e,description:t}),l||(n.description=t),n},ae=function(e,t,n){e===W&&ae(ne,t,n),f(e);var r=m(t);return f(n),p(te,r)?(n.enumerable?(p(e,$)&&e[$][r]&&(e[$][r]=!1),n=v(n,{enumerable:y(0,!1)})):(p(e,$)||Y(e,$,y(1,{})),e[$][r]=!0),se(e,r,n)):Y(e,r,n)},le=function(e,t){f(e);var n=d(t),r=b(n).concat(he(n));return B(r,(function(t){l&&!s(ce,n,t)||ae(e,t,n[t])})),e},ce=function(e){var t=m(e),n=s(Q,this,t);return!(this===W&&p(te,t)&&!p(ne,t))&&(!(n||!p(this,t)||!p(te,t)||p(this,$)&&this[$][t])||n)},ue=function(e,t){var n=d(e),r=m(t);if(n!==W||!p(te,r)||p(ne,r)){var o=Z(n,r);return!o||!p(te,r)||p(n,$)&&n[$][r]||(o.enumerable=!0),o}},pe=function(e){var t=X(d(e)),n=[];return B(t,(function(e){p(te,e)||p(N,e)||ee(n,e)})),n},he=function(e){var t=e===W,n=X(t?ne:d(e)),r=[];return B(n,(function(e){!p(te,e)||t&&!p(W,e)||ee(r,te[e])})),r};c||(k(K=(J=function(){if(h(K,this))throw H("Symbol is not a constructor");var e=arguments.length&&void 0!==arguments[0]?g(arguments[0]):void 0,t=I(e),n=function(e){this===W&&s(n,ne,e),p(this,$)&&p(this[$],t)&&(this[$][t]=!1),se(this,t,y(1,e))};return l&&oe&&se(W,t,{configurable:!0,set:n}),ie(t,e)})[U],"toString",(function(){return V(this).tag})),k(J,"withoutSetter",(function(e){return ie(I(e),e)})),O.f=ce,_.f=ae,j.f=le,S.f=ue,w.f=E.f=pe,x.f=he,R.f=function(e){return ie(T(e),e)},l&&(A(K,"description",{configurable:!0,get:function(){return V(this).description}}),a||k(W,"propertyIsEnumerable",ce,{unsafe:!0}))),r({global:!0,constructor:!0,wrap:!0,forced:!c,sham:!c},{Symbol:J}),B(b(re),(function(e){M(e)})),r({target:q,stat:!0,forced:!c},{useSetter:function(){oe=!0},useSimple:function(){oe=!1}}),r({target:"Object",stat:!0,forced:!c,sham:!l},{create:function(e,t){return void 0===t?v(e):le(v(e),t)},defineProperty:ae,defineProperties:le,getOwnPropertyDescriptor:ue}),r({target:"Object",stat:!0,forced:!c},{getOwnPropertyNames:pe}),D(),F(J,q),N[$]=!0},52615:()=>{},64523:(e,t,n)=>{var r=n(76887),o=n(626),s=n(90953),i=n(85803),a=n(68726),l=n(34680),c=a("string-to-symbol-registry"),u=a("symbol-to-string-registry");r({target:"Symbol",stat:!0,forced:!l},{for:function(e){var t=i(e);if(s(c,t))return c[t];var n=o("Symbol")(t);return c[t]=n,u[n]=t,n}})},21732:(e,t,n)=>{n(73464)("hasInstance")},35903:(e,t,n)=>{n(73464)("isConcatSpreadable")},1825:(e,t,n)=>{n(73464)("iterator")},35824:(e,t,n)=>{n(48616),n(64523),n(38608),n(32619),n(37144)},38608:(e,t,n)=>{var r=n(76887),o=n(90953),s=n(56664),i=n(69826),a=n(68726),l=n(34680),c=a("symbol-to-string-registry");r({target:"Symbol",stat:!0,forced:!l},{keyFor:function(e){if(!s(e))throw TypeError(i(e)+" is not a symbol");if(o(c,e))return c[e]}})},45915:(e,t,n)=>{n(73464)("matchAll")},28394:(e,t,n)=>{n(73464)("match")},61766:(e,t,n)=>{n(73464)("replace")},62737:(e,t,n)=>{n(73464)("search")},89911:(e,t,n)=>{n(73464)("species")},74315:(e,t,n)=>{n(73464)("split")},63131:(e,t,n)=>{var r=n(73464),o=n(29630);r("toPrimitive"),o()},64714:(e,t,n)=>{var r=n(626),o=n(73464),s=n(90904);o("toStringTag"),s(r("Symbol"),"Symbol")},70659:(e,t,n)=>{n(73464)("unscopables")},94776:(e,t,n)=>{"use strict";var r,o=n(45602),s=n(21899),i=n(95329),a=n(94380),l=n(21647),c=n(24683),u=n(8850),p=n(10941),h=n(45402).enforce,f=n(95981),d=n(47093),m=Object,g=Array.isArray,y=m.isExtensible,v=m.isFrozen,b=m.isSealed,w=m.freeze,E=m.seal,x={},S={},_=!s.ActiveXObject&&"ActiveXObject"in s,j=function(e){return function(){return e(this,arguments.length?arguments[0]:void 0)}},O=c("WeakMap",j,u),k=O.prototype,A=i(k.set);if(d)if(_){r=u.getConstructor(j,"WeakMap",!0),l.enable();var C=i(k.delete),P=i(k.has),N=i(k.get);a(k,{delete:function(e){if(p(e)&&!y(e)){var t=h(this);return t.frozen||(t.frozen=new r),C(this,e)||t.frozen.delete(e)}return C(this,e)},has:function(e){if(p(e)&&!y(e)){var t=h(this);return t.frozen||(t.frozen=new r),P(this,e)||t.frozen.has(e)}return P(this,e)},get:function(e){if(p(e)&&!y(e)){var t=h(this);return t.frozen||(t.frozen=new r),P(this,e)?N(this,e):t.frozen.get(e)}return N(this,e)},set:function(e,t){if(p(e)&&!y(e)){var n=h(this);n.frozen||(n.frozen=new r),P(this,e)?A(this,e,t):n.frozen.set(e,t)}else A(this,e,t);return this}})}else o&&f((function(){var e=w([]);return A(new O,e,1),!v(e)}))&&a(k,{set:function(e,t){var n;return g(e)&&(v(e)?n=x:b(e)&&(n=S)),A(this,e,t),n==x&&w(e),n==S&&E(e),this}})},54334:(e,t,n)=>{n(94776)},31115:(e,t,n)=>{"use strict";n(24683)("WeakSet",(function(e){return function(){return e(this,arguments.length?arguments[0]:void 0)}}),n(8850))},1773:(e,t,n)=>{n(31115)},97522:(e,t,n)=>{var r=n(99813),o=n(65988).f,s=r("metadata"),i=Function.prototype;void 0===i[s]&&o(i,s,{value:null})},28783:(e,t,n)=>{n(73464)("asyncDispose")},43975:(e,t,n)=>{n(73464)("dispose")},97618:(e,t,n)=>{n(76887)({target:"Symbol",stat:!0},{isRegisteredSymbol:n(32087)})},22731:(e,t,n)=>{n(76887)({target:"Symbol",stat:!0,name:"isRegisteredSymbol"},{isRegistered:n(32087)})},6989:(e,t,n)=>{n(76887)({target:"Symbol",stat:!0,forced:!0},{isWellKnownSymbol:n(96559)})},85605:(e,t,n)=>{n(76887)({target:"Symbol",stat:!0,name:"isWellKnownSymbol",forced:!0},{isWellKnown:n(96559)})},65799:(e,t,n)=>{n(73464)("matcher")},31943:(e,t,n)=>{n(73464)("metadataKey")},45414:(e,t,n)=>{n(73464)("metadata")},46774:(e,t,n)=>{n(73464)("observable")},80620:(e,t,n)=>{n(73464)("patternMatch")},36172:(e,t,n)=>{n(73464)("replaceAll")},7634:(e,t,n)=>{n(66274);var r=n(63281),o=n(21899),s=n(9697),i=n(32029),a=n(12077),l=n(99813)("toStringTag");for(var c in r){var u=o[c],p=u&&u.prototype;p&&s(p)!==l&&i(p,l,c),a[c]=a.Array}},79229:(e,t,n)=>{var r=n(76887),o=n(21899),s=n(37620)(o.setInterval,!0);r({global:!0,bind:!0,forced:o.setInterval!==s},{setInterval:s})},17749:(e,t,n)=>{var r=n(76887),o=n(21899),s=n(37620)(o.setTimeout,!0);r({global:!0,bind:!0,forced:o.setTimeout!==s},{setTimeout:s})},71249:(e,t,n)=>{n(79229),n(17749)},62524:(e,t,n)=>{"use strict";n(66274);var r=n(76887),o=n(21899),s=n(78834),i=n(95329),a=n(55746),l=n(14766),c=n(95929),u=n(29202),p=n(94380),h=n(90904),f=n(53847),d=n(45402),m=n(5743),g=n(57475),y=n(90953),v=n(86843),b=n(9697),w=n(96059),E=n(10941),x=n(85803),S=n(29290),_=n(31887),j=n(53476),O=n(22902),k=n(18348),A=n(99813),C=n(61388),P=A("iterator"),N="URLSearchParams",I=N+"Iterator",T=d.set,R=d.getterFor(N),M=d.getterFor(I),D=Object.getOwnPropertyDescriptor,F=function(e){if(!a)return o[e];var t=D(o,e);return t&&t.value},L=F("fetch"),B=F("Request"),$=F("Headers"),q=B&&B.prototype,U=$&&$.prototype,z=o.RegExp,V=o.TypeError,W=o.decodeURIComponent,J=o.encodeURIComponent,K=i("".charAt),H=i([].join),G=i([].push),Z=i("".replace),Y=i([].shift),X=i([].splice),Q=i("".split),ee=i("".slice),te=/\+/g,ne=Array(4),re=function(e){return ne[e-1]||(ne[e-1]=z("((?:%[\\da-f]{2}){"+e+"})","gi"))},oe=function(e){try{return W(e)}catch(t){return e}},se=function(e){var t=Z(e,te," "),n=4;try{return W(t)}catch(e){for(;n;)t=Z(t,re(n--),oe);return t}},ie=/[!'()~]|%20/g,ae={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+"},le=function(e){return ae[e]},ce=function(e){return Z(J(e),ie,le)},ue=f((function(e,t){T(this,{type:I,iterator:j(R(e).entries),kind:t})}),"Iterator",(function(){var e=M(this),t=e.kind,n=e.iterator.next(),r=n.value;return n.done||(n.value="keys"===t?r.key:"values"===t?r.value:[r.key,r.value]),n}),!0),pe=function(e){this.entries=[],this.url=null,void 0!==e&&(E(e)?this.parseObject(e):this.parseQuery("string"==typeof e?"?"===K(e,0)?ee(e,1):e:x(e)))};pe.prototype={type:N,bindURL:function(e){this.url=e,this.update()},parseObject:function(e){var t,n,r,o,i,a,l,c=O(e);if(c)for(n=(t=j(e,c)).next;!(r=s(n,t)).done;){if(i=(o=j(w(r.value))).next,(a=s(i,o)).done||(l=s(i,o)).done||!s(i,o).done)throw V("Expected sequence with length 2");G(this.entries,{key:x(a.value),value:x(l.value)})}else for(var u in e)y(e,u)&&G(this.entries,{key:u,value:x(e[u])})},parseQuery:function(e){if(e)for(var t,n,r=Q(e,"&"),o=0;o0?arguments[0]:void 0));a||(this.size=e.entries.length)},fe=he.prototype;if(p(fe,{append:function(e,t){var n=R(this);k(arguments.length,2),G(n.entries,{key:x(e),value:x(t)}),a||this.length++,n.updateURL()},delete:function(e){for(var t=R(this),n=k(arguments.length,1),r=t.entries,o=x(e),s=n<2?void 0:arguments[1],i=void 0===s?s:x(s),l=0;lt.key?1:-1})),e.updateURL()},forEach:function(e){for(var t,n=R(this).entries,r=v(e,arguments.length>1?arguments[1]:void 0),o=0;o1?ge(arguments[1]):{})}}),g(B)){var ye=function(e){return m(this,q),new B(e,arguments.length>1?ge(arguments[1]):{})};q.constructor=ye,ye.prototype=q,r({global:!0,constructor:!0,dontCallGetSet:!0,forced:!0},{Request:ye})}}e.exports={URLSearchParams:he,getState:R}},16454:()=>{},73305:()=>{},95304:(e,t,n)=>{n(62524)},62337:()=>{},84630:(e,t,n)=>{var r=n(76887),o=n(626),s=n(95981),i=n(18348),a=n(85803),l=n(14766),c=o("URL");r({target:"URL",stat:!0,forced:!(l&&s((function(){c.canParse()})))},{canParse:function(e){var t=i(arguments.length,1),n=a(e),r=t<2||void 0===arguments[1]?void 0:a(arguments[1]);try{return!!new c(n,r)}catch(e){return!1}}})},47250:(e,t,n)=>{"use strict";n(77971);var r,o=n(76887),s=n(55746),i=n(14766),a=n(21899),l=n(86843),c=n(95329),u=n(95929),p=n(29202),h=n(5743),f=n(90953),d=n(24420),m=n(11354),g=n(15790),y=n(64620).codeAt,v=n(73291),b=n(85803),w=n(90904),E=n(18348),x=n(62524),S=n(45402),_=S.set,j=S.getterFor("URL"),O=x.URLSearchParams,k=x.getState,A=a.URL,C=a.TypeError,P=a.parseInt,N=Math.floor,I=Math.pow,T=c("".charAt),R=c(/./.exec),M=c([].join),D=c(1..toString),F=c([].pop),L=c([].push),B=c("".replace),$=c([].shift),q=c("".split),U=c("".slice),z=c("".toLowerCase),V=c([].unshift),W="Invalid scheme",J="Invalid host",K="Invalid port",H=/[a-z]/i,G=/[\d+-.a-z]/i,Z=/\d/,Y=/^0x/i,X=/^[0-7]+$/,Q=/^\d+$/,ee=/^[\da-f]+$/i,te=/[\0\t\n\r #%/:<>?@[\\\]^|]/,ne=/[\0\t\n\r #/:<>?@[\\\]^|]/,re=/^[\u0000-\u0020]+/,oe=/(^|[^\u0000-\u0020])[\u0000-\u0020]+$/,se=/[\t\n\r]/g,ie=function(e){var t,n,r,o;if("number"==typeof e){for(t=[],n=0;n<4;n++)V(t,e%256),e=N(e/256);return M(t,".")}if("object"==typeof e){for(t="",r=function(e){for(var t=null,n=1,r=null,o=0,s=0;s<8;s++)0!==e[s]?(o>n&&(t=r,n=o),r=null,o=0):(null===r&&(r=s),++o);return o>n&&(t=r,n=o),t}(e),n=0;n<8;n++)o&&0===e[n]||(o&&(o=!1),r===n?(t+=n?":":"::",o=!0):(t+=D(e[n],16),n<7&&(t+=":")));return"["+t+"]"}return e},ae={},le=d({},ae,{" ":1,'"':1,"<":1,">":1,"`":1}),ce=d({},le,{"#":1,"?":1,"{":1,"}":1}),ue=d({},ce,{"/":1,":":1,";":1,"=":1,"@":1,"[":1,"\\":1,"]":1,"^":1,"|":1}),pe=function(e,t){var n=y(e,0);return n>32&&n<127&&!f(t,e)?e:encodeURIComponent(e)},he={ftp:21,file:null,http:80,https:443,ws:80,wss:443},fe=function(e,t){var n;return 2==e.length&&R(H,T(e,0))&&(":"==(n=T(e,1))||!t&&"|"==n)},de=function(e){var t;return e.length>1&&fe(U(e,0,2))&&(2==e.length||"/"===(t=T(e,2))||"\\"===t||"?"===t||"#"===t)},me=function(e){return"."===e||"%2e"===z(e)},ge={},ye={},ve={},be={},we={},Ee={},xe={},Se={},_e={},je={},Oe={},ke={},Ae={},Ce={},Pe={},Ne={},Ie={},Te={},Re={},Me={},De={},Fe=function(e,t,n){var r,o,s,i=b(e);if(t){if(o=this.parse(i))throw C(o);this.searchParams=null}else{if(void 0!==n&&(r=new Fe(n,!0)),o=this.parse(i,null,r))throw C(o);(s=k(new O)).bindURL(this),this.searchParams=s}};Fe.prototype={type:"URL",parse:function(e,t,n){var o,s,i,a,l,c=this,u=t||ge,p=0,h="",d=!1,y=!1,v=!1;for(e=b(e),t||(c.scheme="",c.username="",c.password="",c.host=null,c.port=null,c.path=[],c.query=null,c.fragment=null,c.cannotBeABaseURL=!1,e=B(e,re,""),e=B(e,oe,"$1")),e=B(e,se,""),o=m(e);p<=o.length;){switch(s=o[p],u){case ge:if(!s||!R(H,s)){if(t)return W;u=ve;continue}h+=z(s),u=ye;break;case ye:if(s&&(R(G,s)||"+"==s||"-"==s||"."==s))h+=z(s);else{if(":"!=s){if(t)return W;h="",u=ve,p=0;continue}if(t&&(c.isSpecial()!=f(he,h)||"file"==h&&(c.includesCredentials()||null!==c.port)||"file"==c.scheme&&!c.host))return;if(c.scheme=h,t)return void(c.isSpecial()&&he[c.scheme]==c.port&&(c.port=null));h="","file"==c.scheme?u=Ce:c.isSpecial()&&n&&n.scheme==c.scheme?u=be:c.isSpecial()?u=Se:"/"==o[p+1]?(u=we,p++):(c.cannotBeABaseURL=!0,L(c.path,""),u=Re)}break;case ve:if(!n||n.cannotBeABaseURL&&"#"!=s)return W;if(n.cannotBeABaseURL&&"#"==s){c.scheme=n.scheme,c.path=g(n.path),c.query=n.query,c.fragment="",c.cannotBeABaseURL=!0,u=De;break}u="file"==n.scheme?Ce:Ee;continue;case be:if("/"!=s||"/"!=o[p+1]){u=Ee;continue}u=_e,p++;break;case we:if("/"==s){u=je;break}u=Te;continue;case Ee:if(c.scheme=n.scheme,s==r)c.username=n.username,c.password=n.password,c.host=n.host,c.port=n.port,c.path=g(n.path),c.query=n.query;else if("/"==s||"\\"==s&&c.isSpecial())u=xe;else if("?"==s)c.username=n.username,c.password=n.password,c.host=n.host,c.port=n.port,c.path=g(n.path),c.query="",u=Me;else{if("#"!=s){c.username=n.username,c.password=n.password,c.host=n.host,c.port=n.port,c.path=g(n.path),c.path.length--,u=Te;continue}c.username=n.username,c.password=n.password,c.host=n.host,c.port=n.port,c.path=g(n.path),c.query=n.query,c.fragment="",u=De}break;case xe:if(!c.isSpecial()||"/"!=s&&"\\"!=s){if("/"!=s){c.username=n.username,c.password=n.password,c.host=n.host,c.port=n.port,u=Te;continue}u=je}else u=_e;break;case Se:if(u=_e,"/"!=s||"/"!=T(h,p+1))continue;p++;break;case _e:if("/"!=s&&"\\"!=s){u=je;continue}break;case je:if("@"==s){d&&(h="%40"+h),d=!0,i=m(h);for(var w=0;w65535)return K;c.port=c.isSpecial()&&S===he[c.scheme]?null:S,h=""}if(t)return;u=Ie;continue}return K}h+=s;break;case Ce:if(c.scheme="file","/"==s||"\\"==s)u=Pe;else{if(!n||"file"!=n.scheme){u=Te;continue}if(s==r)c.host=n.host,c.path=g(n.path),c.query=n.query;else if("?"==s)c.host=n.host,c.path=g(n.path),c.query="",u=Me;else{if("#"!=s){de(M(g(o,p),""))||(c.host=n.host,c.path=g(n.path),c.shortenPath()),u=Te;continue}c.host=n.host,c.path=g(n.path),c.query=n.query,c.fragment="",u=De}}break;case Pe:if("/"==s||"\\"==s){u=Ne;break}n&&"file"==n.scheme&&!de(M(g(o,p),""))&&(fe(n.path[0],!0)?L(c.path,n.path[0]):c.host=n.host),u=Te;continue;case Ne:if(s==r||"/"==s||"\\"==s||"?"==s||"#"==s){if(!t&&fe(h))u=Te;else if(""==h){if(c.host="",t)return;u=Ie}else{if(a=c.parseHost(h))return a;if("localhost"==c.host&&(c.host=""),t)return;h="",u=Ie}continue}h+=s;break;case Ie:if(c.isSpecial()){if(u=Te,"/"!=s&&"\\"!=s)continue}else if(t||"?"!=s)if(t||"#"!=s){if(s!=r&&(u=Te,"/"!=s))continue}else c.fragment="",u=De;else c.query="",u=Me;break;case Te:if(s==r||"/"==s||"\\"==s&&c.isSpecial()||!t&&("?"==s||"#"==s)){if(".."===(l=z(l=h))||"%2e."===l||".%2e"===l||"%2e%2e"===l?(c.shortenPath(),"/"==s||"\\"==s&&c.isSpecial()||L(c.path,"")):me(h)?"/"==s||"\\"==s&&c.isSpecial()||L(c.path,""):("file"==c.scheme&&!c.path.length&&fe(h)&&(c.host&&(c.host=""),h=T(h,0)+":"),L(c.path,h)),h="","file"==c.scheme&&(s==r||"?"==s||"#"==s))for(;c.path.length>1&&""===c.path[0];)$(c.path);"?"==s?(c.query="",u=Me):"#"==s&&(c.fragment="",u=De)}else h+=pe(s,ce);break;case Re:"?"==s?(c.query="",u=Me):"#"==s?(c.fragment="",u=De):s!=r&&(c.path[0]+=pe(s,ae));break;case Me:t||"#"!=s?s!=r&&("'"==s&&c.isSpecial()?c.query+="%27":c.query+="#"==s?"%23":pe(s,ae)):(c.fragment="",u=De);break;case De:s!=r&&(c.fragment+=pe(s,le))}p++}},parseHost:function(e){var t,n,r;if("["==T(e,0)){if("]"!=T(e,e.length-1))return J;if(t=function(e){var t,n,r,o,s,i,a,l=[0,0,0,0,0,0,0,0],c=0,u=null,p=0,h=function(){return T(e,p)};if(":"==h()){if(":"!=T(e,1))return;p+=2,u=++c}for(;h();){if(8==c)return;if(":"!=h()){for(t=n=0;n<4&&R(ee,h());)t=16*t+P(h(),16),p++,n++;if("."==h()){if(0==n)return;if(p-=n,c>6)return;for(r=0;h();){if(o=null,r>0){if(!("."==h()&&r<4))return;p++}if(!R(Z,h()))return;for(;R(Z,h());){if(s=P(h(),10),null===o)o=s;else{if(0==o)return;o=10*o+s}if(o>255)return;p++}l[c]=256*l[c]+o,2!=++r&&4!=r||c++}if(4!=r)return;break}if(":"==h()){if(p++,!h())return}else if(h())return;l[c++]=t}else{if(null!==u)return;p++,u=++c}}if(null!==u)for(i=c-u,c=7;0!=c&&i>0;)a=l[c],l[c--]=l[u+i-1],l[u+--i]=a;else if(8!=c)return;return l}(U(e,1,-1)),!t)return J;this.host=t}else if(this.isSpecial()){if(e=v(e),R(te,e))return J;if(t=function(e){var t,n,r,o,s,i,a,l=q(e,".");if(l.length&&""==l[l.length-1]&&l.length--,(t=l.length)>4)return e;for(n=[],r=0;r1&&"0"==T(o,0)&&(s=R(Y,o)?16:8,o=U(o,8==s?1:2)),""===o)i=0;else{if(!R(10==s?Q:8==s?X:ee,o))return e;i=P(o,s)}L(n,i)}for(r=0;r=I(256,5-t))return null}else if(i>255)return null;for(a=F(n),r=0;r1?arguments[1]:void 0,r=_(t,new Fe(e,!1,n));s||(t.href=r.serialize(),t.origin=r.getOrigin(),t.protocol=r.getProtocol(),t.username=r.getUsername(),t.password=r.getPassword(),t.host=r.getHost(),t.hostname=r.getHostname(),t.port=r.getPort(),t.pathname=r.getPathname(),t.search=r.getSearch(),t.searchParams=r.getSearchParams(),t.hash=r.getHash())},Be=Le.prototype,$e=function(e,t){return{get:function(){return j(this)[e]()},set:t&&function(e){return j(this)[t](e)},configurable:!0,enumerable:!0}};if(s&&(p(Be,"href",$e("serialize","setHref")),p(Be,"origin",$e("getOrigin")),p(Be,"protocol",$e("getProtocol","setProtocol")),p(Be,"username",$e("getUsername","setUsername")),p(Be,"password",$e("getPassword","setPassword")),p(Be,"host",$e("getHost","setHost")),p(Be,"hostname",$e("getHostname","setHostname")),p(Be,"port",$e("getPort","setPort")),p(Be,"pathname",$e("getPathname","setPathname")),p(Be,"search",$e("getSearch","setSearch")),p(Be,"searchParams",$e("getSearchParams")),p(Be,"hash",$e("getHash","setHash"))),u(Be,"toJSON",(function(){return j(this).serialize()}),{enumerable:!0}),u(Be,"toString",(function(){return j(this).serialize()}),{enumerable:!0}),A){var qe=A.createObjectURL,Ue=A.revokeObjectURL;qe&&u(Le,"createObjectURL",l(qe,A)),Ue&&u(Le,"revokeObjectURL",l(Ue,A))}w(Le,"URL"),o({global:!0,constructor:!0,forced:!i,sham:!s},{URL:Le})},33601:(e,t,n)=>{n(47250)},98947:()=>{},24848:(e,t,n)=>{var r=n(54493);e.exports=r},83363:(e,t,n)=>{var r=n(24034);e.exports=r},62908:(e,t,n)=>{var r=n(12710);e.exports=r},49216:(e,t,n)=>{var r=n(99324);e.exports=r},56668:(e,t,n)=>{var r=n(95909);e.exports=r},74719:(e,t,n)=>{var r=n(14423);e.exports=r},57784:(e,t,n)=>{var r=n(81103);e.exports=r},28196:(e,t,n)=>{var r=n(16246);e.exports=r},8065:(e,t,n)=>{var r=n(56043);e.exports=r},57448:(e,t,n)=>{n(7634);var r=n(9697),o=n(90953),s=n(7046),i=n(62908),a=Array.prototype,l={DOMTokenList:!0,NodeList:!0};e.exports=function(e){var t=e.entries;return e===a||s(a,e)&&t===a.entries||o(l,r(e))?i:t}},29455:(e,t,n)=>{var r=n(13160);e.exports=r},69743:(e,t,n)=>{var r=n(80446);e.exports=r},11955:(e,t,n)=>{var r=n(2480);e.exports=r},96064:(e,t,n)=>{var r=n(7147);e.exports=r},61577:(e,t,n)=>{var r=n(32236);e.exports=r},46279:(e,t,n)=>{n(7634);var r=n(9697),o=n(90953),s=n(7046),i=n(49216),a=Array.prototype,l={DOMTokenList:!0,NodeList:!0};e.exports=function(e){var t=e.forEach;return e===a||s(a,e)&&t===a.forEach||o(l,r(e))?i:t}},33778:(e,t,n)=>{var r=n(58557);e.exports=r},19373:(e,t,n)=>{var r=n(34570);e.exports=r},73819:(e,t,n)=>{n(7634);var r=n(9697),o=n(90953),s=n(7046),i=n(56668),a=Array.prototype,l={DOMTokenList:!0,NodeList:!0};e.exports=function(e){var t=e.keys;return e===a||s(a,e)&&t===a.keys||o(l,r(e))?i:t}},11022:(e,t,n)=>{var r=n(57564);e.exports=r},61798:(e,t,n)=>{var r=n(88287);e.exports=r},52759:(e,t,n)=>{var r=n(93993);e.exports=r},52527:(e,t,n)=>{var r=n(68025);e.exports=r},36857:(e,t,n)=>{var r=n(59257);e.exports=r},82073:(e,t,n)=>{var r=n(69601);e.exports=r},45286:(e,t,n)=>{var r=n(28299);e.exports=r},62856:(e,t,n)=>{var r=n(69355);e.exports=r},2348:(e,t,n)=>{var r=n(18339);e.exports=r},35178:(e,t,n)=>{var r=n(71611);e.exports=r},76361:(e,t,n)=>{var r=n(62774);e.exports=r},71815:(e,t,n)=>{n(7634);var r=n(9697),o=n(90953),s=n(7046),i=n(74719),a=Array.prototype,l={DOMTokenList:!0,NodeList:!0};e.exports=function(e){var t=e.values;return e===a||s(a,e)&&t===a.values||o(l,r(e))?i:t}},8933:(e,t,n)=>{var r=n(84426);e.exports=r},15868:(e,t,n)=>{var r=n(91018);n(7634),e.exports=r},14873:(e,t,n)=>{var r=n(97849);e.exports=r},38849:(e,t,n)=>{var r=n(3820);e.exports=r},63383:(e,t,n)=>{var r=n(45999);e.exports=r},57396:(e,t,n)=>{var r=n(7702);e.exports=r},41910:(e,t,n)=>{var r=n(48171);e.exports=r},86209:(e,t,n)=>{var r=n(73081);e.exports=r},53402:(e,t,n)=>{var r=n(7699);n(7634),e.exports=r},79427:(e,t,n)=>{var r=n(286);e.exports=r},62857:(e,t,n)=>{var r=n(92766);e.exports=r},9534:(e,t,n)=>{var r=n(30498);e.exports=r},23059:(e,t,n)=>{var r=n(48494);e.exports=r},47795:(e,t,n)=>{var r=n(98430);e.exports=r},27460:(e,t,n)=>{var r=n(52956);n(7634),e.exports=r},27989:(e,t,n)=>{n(71249);var r=n(54058);e.exports=r.setTimeout},5519:(e,t,n)=>{var r=n(76998);n(7634),e.exports=r},23452:(e,t,n)=>{var r=n(97089);e.exports=r},92547:(e,t,n)=>{var r=n(57473);n(7634),e.exports=r},46509:(e,t,n)=>{var r=n(24227);n(7634),e.exports=r},35774:(e,t,n)=>{var r=n(62978);e.exports=r},57641:(e,t,n)=>{var r=n(71459);e.exports=r},72010:(e,t,n)=>{var r=n(32304);n(7634),e.exports=r},93726:(e,t,n)=>{var r=n(29567);n(7634),e.exports=r},47610:(e,t,n)=>{n(95304),n(16454),n(73305),n(62337);var r=n(54058);e.exports=r.URLSearchParams},71459:(e,t,n)=>{n(47610),n(33601),n(84630),n(98947);var r=n(54058);e.exports=r.URL},31905:function(){!function(e){!function(t){var n="URLSearchParams"in e,r="Symbol"in e&&"iterator"in Symbol,o="FileReader"in e&&"Blob"in e&&function(){try{return new Blob,!0}catch(e){return!1}}(),s="FormData"in e,i="ArrayBuffer"in e;if(i)var a=["[object Int8Array]","[object Uint8Array]","[object Uint8ClampedArray]","[object Int16Array]","[object Uint16Array]","[object Int32Array]","[object Uint32Array]","[object Float32Array]","[object Float64Array]"],l=ArrayBuffer.isView||function(e){return e&&a.indexOf(Object.prototype.toString.call(e))>-1};function c(e){if("string"!=typeof e&&(e=String(e)),/[^a-z0-9\-#$%&'*+.^_`|~]/i.test(e))throw new TypeError("Invalid character in header field name");return e.toLowerCase()}function u(e){return"string"!=typeof e&&(e=String(e)),e}function p(e){var t={next:function(){var t=e.shift();return{done:void 0===t,value:t}}};return r&&(t[Symbol.iterator]=function(){return t}),t}function h(e){this.map={},e instanceof h?e.forEach((function(e,t){this.append(t,e)}),this):Array.isArray(e)?e.forEach((function(e){this.append(e[0],e[1])}),this):e&&Object.getOwnPropertyNames(e).forEach((function(t){this.append(t,e[t])}),this)}function f(e){if(e.bodyUsed)return Promise.reject(new TypeError("Already read"));e.bodyUsed=!0}function d(e){return new Promise((function(t,n){e.onload=function(){t(e.result)},e.onerror=function(){n(e.error)}}))}function m(e){var t=new FileReader,n=d(t);return t.readAsArrayBuffer(e),n}function g(e){if(e.slice)return e.slice(0);var t=new Uint8Array(e.byteLength);return t.set(new Uint8Array(e)),t.buffer}function y(){return this.bodyUsed=!1,this._initBody=function(e){var t;this._bodyInit=e,e?"string"==typeof e?this._bodyText=e:o&&Blob.prototype.isPrototypeOf(e)?this._bodyBlob=e:s&&FormData.prototype.isPrototypeOf(e)?this._bodyFormData=e:n&&URLSearchParams.prototype.isPrototypeOf(e)?this._bodyText=e.toString():i&&o&&((t=e)&&DataView.prototype.isPrototypeOf(t))?(this._bodyArrayBuffer=g(e.buffer),this._bodyInit=new Blob([this._bodyArrayBuffer])):i&&(ArrayBuffer.prototype.isPrototypeOf(e)||l(e))?this._bodyArrayBuffer=g(e):this._bodyText=e=Object.prototype.toString.call(e):this._bodyText="",this.headers.get("content-type")||("string"==typeof e?this.headers.set("content-type","text/plain;charset=UTF-8"):this._bodyBlob&&this._bodyBlob.type?this.headers.set("content-type",this._bodyBlob.type):n&&URLSearchParams.prototype.isPrototypeOf(e)&&this.headers.set("content-type","application/x-www-form-urlencoded;charset=UTF-8"))},o&&(this.blob=function(){var e=f(this);if(e)return e;if(this._bodyBlob)return Promise.resolve(this._bodyBlob);if(this._bodyArrayBuffer)return Promise.resolve(new Blob([this._bodyArrayBuffer]));if(this._bodyFormData)throw new Error("could not read FormData body as blob");return Promise.resolve(new Blob([this._bodyText]))},this.arrayBuffer=function(){return this._bodyArrayBuffer?f(this)||Promise.resolve(this._bodyArrayBuffer):this.blob().then(m)}),this.text=function(){var e,t,n,r=f(this);if(r)return r;if(this._bodyBlob)return e=this._bodyBlob,t=new FileReader,n=d(t),t.readAsText(e),n;if(this._bodyArrayBuffer)return Promise.resolve(function(e){for(var t=new Uint8Array(e),n=new Array(t.length),r=0;r-1?r:n),this.mode=t.mode||this.mode||null,this.signal=t.signal||this.signal,this.referrer=null,("GET"===this.method||"HEAD"===this.method)&&o)throw new TypeError("Body not allowed for GET or HEAD requests");this._initBody(o)}function w(e){var t=new FormData;return e.trim().split("&").forEach((function(e){if(e){var n=e.split("="),r=n.shift().replace(/\+/g," "),o=n.join("=").replace(/\+/g," ");t.append(decodeURIComponent(r),decodeURIComponent(o))}})),t}function E(e,t){t||(t={}),this.type="default",this.status=void 0===t.status?200:t.status,this.ok=this.status>=200&&this.status<300,this.statusText="statusText"in t?t.statusText:"OK",this.headers=new h(t.headers),this.url=t.url||"",this._initBody(e)}b.prototype.clone=function(){return new b(this,{body:this._bodyInit})},y.call(b.prototype),y.call(E.prototype),E.prototype.clone=function(){return new E(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new h(this.headers),url:this.url})},E.error=function(){var e=new E(null,{status:0,statusText:""});return e.type="error",e};var x=[301,302,303,307,308];E.redirect=function(e,t){if(-1===x.indexOf(t))throw new RangeError("Invalid status code");return new E(null,{status:t,headers:{location:e}})},t.DOMException=e.DOMException;try{new t.DOMException}catch(e){t.DOMException=function(e,t){this.message=e,this.name=t;var n=Error(e);this.stack=n.stack},t.DOMException.prototype=Object.create(Error.prototype),t.DOMException.prototype.constructor=t.DOMException}function S(e,n){return new Promise((function(r,s){var i=new b(e,n);if(i.signal&&i.signal.aborted)return s(new t.DOMException("Aborted","AbortError"));var a=new XMLHttpRequest;function l(){a.abort()}a.onload=function(){var e,t,n={status:a.status,statusText:a.statusText,headers:(e=a.getAllResponseHeaders()||"",t=new h,e.replace(/\r?\n[\t ]+/g," ").split(/\r?\n/).forEach((function(e){var n=e.split(":"),r=n.shift().trim();if(r){var o=n.join(":").trim();t.append(r,o)}})),t)};n.url="responseURL"in a?a.responseURL:n.headers.get("X-Request-URL");var o="response"in a?a.response:a.responseText;r(new E(o,n))},a.onerror=function(){s(new TypeError("Network request failed"))},a.ontimeout=function(){s(new TypeError("Network request failed"))},a.onabort=function(){s(new t.DOMException("Aborted","AbortError"))},a.open(i.method,i.url,!0),"include"===i.credentials?a.withCredentials=!0:"omit"===i.credentials&&(a.withCredentials=!1),"responseType"in a&&o&&(a.responseType="blob"),i.headers.forEach((function(e,t){a.setRequestHeader(t,e)})),i.signal&&(i.signal.addEventListener("abort",l),a.onreadystatechange=function(){4===a.readyState&&i.signal.removeEventListener("abort",l)}),a.send(void 0===i._bodyInit?null:i._bodyInit)}))}S.polyfill=!0,e.fetch||(e.fetch=S,e.Headers=h,e.Request=b,e.Response=E),t.Headers=h,t.Request=b,t.Response=E,t.fetch=S,Object.defineProperty(t,"__esModule",{value:!0})}({})}("undefined"!=typeof self?self:this)},8269:function(e,t,n){var r;r=void 0!==n.g?n.g:this,e.exports=function(e){if(e.CSS&&e.CSS.escape)return e.CSS.escape;var t=function(e){if(0==arguments.length)throw new TypeError("`CSS.escape` requires an argument.");for(var t,n=String(e),r=n.length,o=-1,s="",i=n.charCodeAt(0);++o=1&&t<=31||127==t||0==o&&t>=48&&t<=57||1==o&&t>=48&&t<=57&&45==i?"\\"+t.toString(16)+" ":0==o&&1==r&&45==t||!(t>=128||45==t||95==t||t>=48&&t<=57||t>=65&&t<=90||t>=97&&t<=122)?"\\"+n.charAt(o):n.charAt(o):s+="�";return s};return e.CSS||(e.CSS={}),e.CSS.escape=t,t}(r)},27698:(e,t,n)=>{"use strict";var r=n(48764).Buffer;function o(e){return e instanceof r||e instanceof Date||e instanceof RegExp}function s(e){if(e instanceof r){var t=r.alloc?r.alloc(e.length):new r(e.length);return e.copy(t),t}if(e instanceof Date)return new Date(e.getTime());if(e instanceof RegExp)return new RegExp(e);throw new Error("Unexpected situation")}function i(e){var t=[];return e.forEach((function(e,n){"object"==typeof e&&null!==e?Array.isArray(e)?t[n]=i(e):o(e)?t[n]=s(e):t[n]=l({},e):t[n]=e})),t}function a(e,t){return"__proto__"===t?void 0:e[t]}var l=e.exports=function(){if(arguments.length<1||"object"!=typeof arguments[0])return!1;if(arguments.length<2)return arguments[0];var e,t,n=arguments[0];return Array.prototype.slice.call(arguments,1).forEach((function(r){"object"!=typeof r||null===r||Array.isArray(r)||Object.keys(r).forEach((function(c){return t=a(n,c),(e=a(r,c))===n?void 0:"object"!=typeof e||null===e?void(n[c]=e):Array.isArray(e)?void(n[c]=i(e)):o(e)?void(n[c]=s(e)):"object"!=typeof t||null===t||Array.isArray(t)?void(n[c]=l({},e)):void(n[c]=l(t,e))}))})),n}},9996:e=>{"use strict";var t=function(e){return function(e){return!!e&&"object"==typeof e}(e)&&!function(e){var t=Object.prototype.toString.call(e);return"[object RegExp]"===t||"[object Date]"===t||function(e){return e.$$typeof===n}(e)}(e)};var n="function"==typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;function r(e,t){return!1!==t.clone&&t.isMergeableObject(e)?l((n=e,Array.isArray(n)?[]:{}),e,t):e;var n}function o(e,t,n){return e.concat(t).map((function(e){return r(e,n)}))}function s(e){return Object.keys(e).concat(function(e){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(e).filter((function(t){return Object.propertyIsEnumerable.call(e,t)})):[]}(e))}function i(e,t){try{return t in e}catch(e){return!1}}function a(e,t,n){var o={};return n.isMergeableObject(e)&&s(e).forEach((function(t){o[t]=r(e[t],n)})),s(t).forEach((function(s){(function(e,t){return i(e,t)&&!(Object.hasOwnProperty.call(e,t)&&Object.propertyIsEnumerable.call(e,t))})(e,s)||(i(e,s)&&n.isMergeableObject(t[s])?o[s]=function(e,t){if(!t.customMerge)return l;var n=t.customMerge(e);return"function"==typeof n?n:l}(s,n)(e[s],t[s],n):o[s]=r(t[s],n))})),o}function l(e,n,s){(s=s||{}).arrayMerge=s.arrayMerge||o,s.isMergeableObject=s.isMergeableObject||t,s.cloneUnlessOtherwiseSpecified=r;var i=Array.isArray(n);return i===Array.isArray(e)?i?s.arrayMerge(e,n,s):a(e,n,s):r(n,s)}l.all=function(e,t){if(!Array.isArray(e))throw new Error("first argument should be an array");return e.reduce((function(e,n){return l(e,n,t)}),{})};var c=l;e.exports=c},27856:function(e){e.exports=function(){"use strict";const{entries:e,setPrototypeOf:t,isFrozen:n,getPrototypeOf:r,getOwnPropertyDescriptor:o}=Object;let{freeze:s,seal:i,create:a}=Object,{apply:l,construct:c}="undefined"!=typeof Reflect&&Reflect;l||(l=function(e,t,n){return e.apply(t,n)}),s||(s=function(e){return e}),i||(i=function(e){return e}),c||(c=function(e,t){return new e(...t)});const u=E(Array.prototype.forEach),p=E(Array.prototype.pop),h=E(Array.prototype.push),f=E(String.prototype.toLowerCase),d=E(String.prototype.toString),m=E(String.prototype.match),g=E(String.prototype.replace),y=E(String.prototype.indexOf),v=E(String.prototype.trim),b=E(RegExp.prototype.test),w=x(TypeError);function E(e){return function(t){for(var n=arguments.length,r=new Array(n>1?n-1:0),o=1;o/gm),B=i(/\${[\w\W]*}/gm),$=i(/^data-[\-\w.\u00B7-\uFFFF]/),q=i(/^aria-[\-\w]+$/),U=i(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),z=i(/^(?:\w+script|data):/i),V=i(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),W=i(/^html$/i);var J=Object.freeze({__proto__:null,MUSTACHE_EXPR:F,ERB_EXPR:L,TMPLIT_EXPR:B,DATA_ATTR:$,ARIA_ATTR:q,IS_ALLOWED_URI:U,IS_SCRIPT_OR_DATA:z,ATTR_WHITESPACE:V,DOCTYPE_NAME:W});const K=()=>"undefined"==typeof window?null:window,H=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const r="data-tt-policy-suffix";t&&t.hasAttribute(r)&&(n=t.getAttribute(r));const o="dompurify"+(n?"#"+n:"");try{return e.createPolicy(o,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+o+" could not be created."),null}};function G(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:K();const n=e=>G(e);if(n.version="3.0.3",n.removed=[],!t||!t.document||9!==t.document.nodeType)return n.isSupported=!1,n;const r=t.document,o=r.currentScript;let{document:i}=t;const{DocumentFragment:a,HTMLTemplateElement:l,Node:c,Element:E,NodeFilter:x,NamedNodeMap:F=t.NamedNodeMap||t.MozNamedAttrMap,HTMLFormElement:L,DOMParser:B,trustedTypes:$}=t,q=E.prototype,z=j(q,"cloneNode"),V=j(q,"nextSibling"),Z=j(q,"childNodes"),Y=j(q,"parentNode");if("function"==typeof l){const e=i.createElement("template");e.content&&e.content.ownerDocument&&(i=e.content.ownerDocument)}let X,Q="";const{implementation:ee,createNodeIterator:te,createDocumentFragment:ne,getElementsByTagName:re}=i,{importNode:oe}=r;let se={};n.isSupported="function"==typeof e&&"function"==typeof Y&&ee&&void 0!==ee.createHTMLDocument;const{MUSTACHE_EXPR:ie,ERB_EXPR:ae,TMPLIT_EXPR:le,DATA_ATTR:ce,ARIA_ATTR:ue,IS_SCRIPT_OR_DATA:pe,ATTR_WHITESPACE:he}=J;let{IS_ALLOWED_URI:fe}=J,de=null;const me=S({},[...O,...k,...A,...P,...I]);let ge=null;const ye=S({},[...T,...R,...M,...D]);let ve=Object.seal(Object.create(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),be=null,we=null,Ee=!0,xe=!0,Se=!1,_e=!0,je=!1,Oe=!1,ke=!1,Ae=!1,Ce=!1,Pe=!1,Ne=!1,Ie=!0,Te=!1;const Re="user-content-";let Me=!0,De=!1,Fe={},Le=null;const Be=S({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let $e=null;const qe=S({},["audio","video","img","source","image","track"]);let Ue=null;const ze=S({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Ve="http://www.w3.org/1998/Math/MathML",We="http://www.w3.org/2000/svg",Je="http://www.w3.org/1999/xhtml";let Ke=Je,He=!1,Ge=null;const Ze=S({},[Ve,We,Je],d);let Ye;const Xe=["application/xhtml+xml","text/html"],Qe="text/html";let et,tt=null;const nt=i.createElement("form"),rt=function(e){return e instanceof RegExp||e instanceof Function},ot=function(e){if(!tt||tt!==e){if(e&&"object"==typeof e||(e={}),e=_(e),Ye=Ye=-1===Xe.indexOf(e.PARSER_MEDIA_TYPE)?Qe:e.PARSER_MEDIA_TYPE,et="application/xhtml+xml"===Ye?d:f,de="ALLOWED_TAGS"in e?S({},e.ALLOWED_TAGS,et):me,ge="ALLOWED_ATTR"in e?S({},e.ALLOWED_ATTR,et):ye,Ge="ALLOWED_NAMESPACES"in e?S({},e.ALLOWED_NAMESPACES,d):Ze,Ue="ADD_URI_SAFE_ATTR"in e?S(_(ze),e.ADD_URI_SAFE_ATTR,et):ze,$e="ADD_DATA_URI_TAGS"in e?S(_(qe),e.ADD_DATA_URI_TAGS,et):qe,Le="FORBID_CONTENTS"in e?S({},e.FORBID_CONTENTS,et):Be,be="FORBID_TAGS"in e?S({},e.FORBID_TAGS,et):{},we="FORBID_ATTR"in e?S({},e.FORBID_ATTR,et):{},Fe="USE_PROFILES"in e&&e.USE_PROFILES,Ee=!1!==e.ALLOW_ARIA_ATTR,xe=!1!==e.ALLOW_DATA_ATTR,Se=e.ALLOW_UNKNOWN_PROTOCOLS||!1,_e=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,je=e.SAFE_FOR_TEMPLATES||!1,Oe=e.WHOLE_DOCUMENT||!1,Ce=e.RETURN_DOM||!1,Pe=e.RETURN_DOM_FRAGMENT||!1,Ne=e.RETURN_TRUSTED_TYPE||!1,Ae=e.FORCE_BODY||!1,Ie=!1!==e.SANITIZE_DOM,Te=e.SANITIZE_NAMED_PROPS||!1,Me=!1!==e.KEEP_CONTENT,De=e.IN_PLACE||!1,fe=e.ALLOWED_URI_REGEXP||U,Ke=e.NAMESPACE||Je,ve=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&rt(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(ve.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&rt(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(ve.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(ve.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),je&&(xe=!1),Pe&&(Ce=!0),Fe&&(de=S({},[...I]),ge=[],!0===Fe.html&&(S(de,O),S(ge,T)),!0===Fe.svg&&(S(de,k),S(ge,R),S(ge,D)),!0===Fe.svgFilters&&(S(de,A),S(ge,R),S(ge,D)),!0===Fe.mathMl&&(S(de,P),S(ge,M),S(ge,D))),e.ADD_TAGS&&(de===me&&(de=_(de)),S(de,e.ADD_TAGS,et)),e.ADD_ATTR&&(ge===ye&&(ge=_(ge)),S(ge,e.ADD_ATTR,et)),e.ADD_URI_SAFE_ATTR&&S(Ue,e.ADD_URI_SAFE_ATTR,et),e.FORBID_CONTENTS&&(Le===Be&&(Le=_(Le)),S(Le,e.FORBID_CONTENTS,et)),Me&&(de["#text"]=!0),Oe&&S(de,["html","head","body"]),de.table&&(S(de,["tbody"]),delete be.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw w('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw w('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');X=e.TRUSTED_TYPES_POLICY,Q=X.createHTML("")}else void 0===X&&(X=H($,o)),null!==X&&"string"==typeof Q&&(Q=X.createHTML(""));s&&s(e),tt=e}},st=S({},["mi","mo","mn","ms","mtext"]),it=S({},["foreignobject","desc","title","annotation-xml"]),at=S({},["title","style","font","a","script"]),lt=S({},k);S(lt,A),S(lt,C);const ct=S({},P);S(ct,N);const ut=function(e){let t=Y(e);t&&t.tagName||(t={namespaceURI:Ke,tagName:"template"});const n=f(e.tagName),r=f(t.tagName);return!!Ge[e.namespaceURI]&&(e.namespaceURI===We?t.namespaceURI===Je?"svg"===n:t.namespaceURI===Ve?"svg"===n&&("annotation-xml"===r||st[r]):Boolean(lt[n]):e.namespaceURI===Ve?t.namespaceURI===Je?"math"===n:t.namespaceURI===We?"math"===n&&it[r]:Boolean(ct[n]):e.namespaceURI===Je?!(t.namespaceURI===We&&!it[r])&&!(t.namespaceURI===Ve&&!st[r])&&!ct[n]&&(at[n]||!lt[n]):!("application/xhtml+xml"!==Ye||!Ge[e.namespaceURI]))},pt=function(e){h(n.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){e.remove()}},ht=function(e,t){try{h(n.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){h(n.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!ge[e])if(Ce||Pe)try{pt(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},ft=function(e){let t,n;if(Ae)e=""+e;else{const t=m(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===Ye&&Ke===Je&&(e=''+e+"");const r=X?X.createHTML(e):e;if(Ke===Je)try{t=(new B).parseFromString(r,Ye)}catch(e){}if(!t||!t.documentElement){t=ee.createDocument(Ke,"template",null);try{t.documentElement.innerHTML=He?Q:r}catch(e){}}const o=t.body||t.documentElement;return e&&n&&o.insertBefore(i.createTextNode(n),o.childNodes[0]||null),Ke===Je?re.call(t,Oe?"html":"body")[0]:Oe?t.documentElement:o},dt=function(e){return te.call(e.ownerDocument||e,e,x.SHOW_ELEMENT|x.SHOW_COMMENT|x.SHOW_TEXT,null,!1)},mt=function(e){return e instanceof L&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof F)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},gt=function(e){return"object"==typeof c?e instanceof c:e&&"object"==typeof e&&"number"==typeof e.nodeType&&"string"==typeof e.nodeName},yt=function(e,t,r){se[e]&&u(se[e],(e=>{e.call(n,t,r,tt)}))},vt=function(e){let t;if(yt("beforeSanitizeElements",e,null),mt(e))return pt(e),!0;const r=et(e.nodeName);if(yt("uponSanitizeElement",e,{tagName:r,allowedTags:de}),e.hasChildNodes()&&!gt(e.firstElementChild)&&(!gt(e.content)||!gt(e.content.firstElementChild))&&b(/<[/\w]/g,e.innerHTML)&&b(/<[/\w]/g,e.textContent))return pt(e),!0;if(!de[r]||be[r]){if(!be[r]&&wt(r)){if(ve.tagNameCheck instanceof RegExp&&b(ve.tagNameCheck,r))return!1;if(ve.tagNameCheck instanceof Function&&ve.tagNameCheck(r))return!1}if(Me&&!Le[r]){const t=Y(e)||e.parentNode,n=Z(e)||e.childNodes;if(n&&t)for(let r=n.length-1;r>=0;--r)t.insertBefore(z(n[r],!0),V(e))}return pt(e),!0}return e instanceof E&&!ut(e)?(pt(e),!0):"noscript"!==r&&"noembed"!==r||!b(/<\/no(script|embed)/i,e.innerHTML)?(je&&3===e.nodeType&&(t=e.textContent,t=g(t,ie," "),t=g(t,ae," "),t=g(t,le," "),e.textContent!==t&&(h(n.removed,{element:e.cloneNode()}),e.textContent=t)),yt("afterSanitizeElements",e,null),!1):(pt(e),!0)},bt=function(e,t,n){if(Ie&&("id"===t||"name"===t)&&(n in i||n in nt))return!1;if(xe&&!we[t]&&b(ce,t));else if(Ee&&b(ue,t));else if(!ge[t]||we[t]){if(!(wt(e)&&(ve.tagNameCheck instanceof RegExp&&b(ve.tagNameCheck,e)||ve.tagNameCheck instanceof Function&&ve.tagNameCheck(e))&&(ve.attributeNameCheck instanceof RegExp&&b(ve.attributeNameCheck,t)||ve.attributeNameCheck instanceof Function&&ve.attributeNameCheck(t))||"is"===t&&ve.allowCustomizedBuiltInElements&&(ve.tagNameCheck instanceof RegExp&&b(ve.tagNameCheck,n)||ve.tagNameCheck instanceof Function&&ve.tagNameCheck(n))))return!1}else if(Ue[t]);else if(b(fe,g(n,he,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==y(n,"data:")||!$e[e])if(Se&&!b(pe,g(n,he,"")));else if(n)return!1;return!0},wt=function(e){return e.indexOf("-")>0},Et=function(e){let t,r,o,s;yt("beforeSanitizeAttributes",e,null);const{attributes:i}=e;if(!i)return;const a={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:ge};for(s=i.length;s--;){t=i[s];const{name:l,namespaceURI:c}=t;if(r="value"===l?t.value:v(t.value),o=et(l),a.attrName=o,a.attrValue=r,a.keepAttr=!0,a.forceKeepAttr=void 0,yt("uponSanitizeAttribute",e,a),r=a.attrValue,a.forceKeepAttr)continue;if(ht(l,e),!a.keepAttr)continue;if(!_e&&b(/\/>/i,r)){ht(l,e);continue}je&&(r=g(r,ie," "),r=g(r,ae," "),r=g(r,le," "));const u=et(e.nodeName);if(bt(u,o,r)){if(!Te||"id"!==o&&"name"!==o||(ht(l,e),r=Re+r),X&&"object"==typeof $&&"function"==typeof $.getAttributeType)if(c);else switch($.getAttributeType(u,o)){case"TrustedHTML":r=X.createHTML(r);break;case"TrustedScriptURL":r=X.createScriptURL(r)}try{c?e.setAttributeNS(c,l,r):e.setAttribute(l,r),p(n.removed)}catch(e){}}}yt("afterSanitizeAttributes",e,null)},xt=function e(t){let n;const r=dt(t);for(yt("beforeSanitizeShadowDOM",t,null);n=r.nextNode();)yt("uponSanitizeShadowNode",n,null),vt(n)||(n.content instanceof a&&e(n.content),Et(n));yt("afterSanitizeShadowDOM",t,null)};return n.sanitize=function(e){let t,o,s,i,l=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(He=!e,He&&(e="\x3c!--\x3e"),"string"!=typeof e&&!gt(e)){if("function"!=typeof e.toString)throw w("toString is not a function");if("string"!=typeof(e=e.toString()))throw w("dirty is not a string, aborting")}if(!n.isSupported)return e;if(ke||ot(l),n.removed=[],"string"==typeof e&&(De=!1),De){if(e.nodeName){const t=et(e.nodeName);if(!de[t]||be[t])throw w("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof c)t=ft("\x3c!----\x3e"),o=t.ownerDocument.importNode(e,!0),1===o.nodeType&&"BODY"===o.nodeName||"HTML"===o.nodeName?t=o:t.appendChild(o);else{if(!Ce&&!je&&!Oe&&-1===e.indexOf("<"))return X&&Ne?X.createHTML(e):e;if(t=ft(e),!t)return Ce?null:Ne?Q:""}t&&Ae&&pt(t.firstChild);const u=dt(De?e:t);for(;s=u.nextNode();)vt(s)||(s.content instanceof a&&xt(s.content),Et(s));if(De)return e;if(Ce){if(Pe)for(i=ne.call(t.ownerDocument);t.firstChild;)i.appendChild(t.firstChild);else i=t;return(ge.shadowroot||ge.shadowrootmod)&&(i=oe.call(r,i,!0)),i}let p=Oe?t.outerHTML:t.innerHTML;return Oe&&de["!doctype"]&&t.ownerDocument&&t.ownerDocument.doctype&&t.ownerDocument.doctype.name&&b(W,t.ownerDocument.doctype.name)&&(p="\n"+p),je&&(p=g(p,ie," "),p=g(p,ae," "),p=g(p,le," ")),X&&Ne?X.createHTML(p):p},n.setConfig=function(e){ot(e),ke=!0},n.clearConfig=function(){tt=null,ke=!1},n.isValidAttribute=function(e,t,n){tt||ot({});const r=et(e),o=et(t);return bt(r,o,n)},n.addHook=function(e,t){"function"==typeof t&&(se[e]=se[e]||[],h(se[e],t))},n.removeHook=function(e){if(se[e])return p(se[e])},n.removeHooks=function(e){se[e]&&(se[e]=[])},n.removeAllHooks=function(){se={}},n}return G()}()},69450:e=>{"use strict";class t{constructor(e,t){this.low=e,this.high=t,this.length=1+t-e}overlaps(e){return!(this.highe.high)}touches(e){return!(this.high+1e.high)}add(e){return new t(Math.min(this.low,e.low),Math.max(this.high,e.high))}subtract(e){return e.low<=this.low&&e.high>=this.high?[]:e.low>this.low&&e.highe+t.length),0)}add(e,r){var o=e=>{for(var t=0;t{for(var t=0;t{for(var n=0;n{for(var n=t.low;n<=t.high;)e.push(n),n++;return e}),[])}subranges(){return this.ranges.map((e=>({low:e.low,high:e.high,length:1+e.high-e.low})))}}e.exports=n},17187:e=>{"use strict";var t,n="object"==typeof Reflect?Reflect:null,r=n&&"function"==typeof n.apply?n.apply:function(e,t,n){return Function.prototype.apply.call(e,t,n)};t=n&&"function"==typeof n.ownKeys?n.ownKeys:Object.getOwnPropertySymbols?function(e){return Object.getOwnPropertyNames(e).concat(Object.getOwnPropertySymbols(e))}:function(e){return Object.getOwnPropertyNames(e)};var o=Number.isNaN||function(e){return e!=e};function s(){s.init.call(this)}e.exports=s,e.exports.once=function(e,t){return new Promise((function(n,r){function o(n){e.removeListener(t,s),r(n)}function s(){"function"==typeof e.removeListener&&e.removeListener("error",o),n([].slice.call(arguments))}m(e,t,s,{once:!0}),"error"!==t&&function(e,t,n){"function"==typeof e.on&&m(e,"error",t,n)}(e,o,{once:!0})}))},s.EventEmitter=s,s.prototype._events=void 0,s.prototype._eventsCount=0,s.prototype._maxListeners=void 0;var i=10;function a(e){if("function"!=typeof e)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof e)}function l(e){return void 0===e._maxListeners?s.defaultMaxListeners:e._maxListeners}function c(e,t,n,r){var o,s,i,c;if(a(n),void 0===(s=e._events)?(s=e._events=Object.create(null),e._eventsCount=0):(void 0!==s.newListener&&(e.emit("newListener",t,n.listener?n.listener:n),s=e._events),i=s[t]),void 0===i)i=s[t]=n,++e._eventsCount;else if("function"==typeof i?i=s[t]=r?[n,i]:[i,n]:r?i.unshift(n):i.push(n),(o=l(e))>0&&i.length>o&&!i.warned){i.warned=!0;var u=new Error("Possible EventEmitter memory leak detected. "+i.length+" "+String(t)+" listeners added. Use emitter.setMaxListeners() to increase limit");u.name="MaxListenersExceededWarning",u.emitter=e,u.type=t,u.count=i.length,c=u,console&&console.warn&&console.warn(c)}return e}function u(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function p(e,t,n){var r={fired:!1,wrapFn:void 0,target:e,type:t,listener:n},o=u.bind(r);return o.listener=n,r.wrapFn=o,o}function h(e,t,n){var r=e._events;if(void 0===r)return[];var o=r[t];return void 0===o?[]:"function"==typeof o?n?[o.listener||o]:[o]:n?function(e){for(var t=new Array(e.length),n=0;n0&&(i=t[0]),i instanceof Error)throw i;var a=new Error("Unhandled error."+(i?" ("+i.message+")":""));throw a.context=i,a}var l=s[e];if(void 0===l)return!1;if("function"==typeof l)r(l,this,t);else{var c=l.length,u=d(l,c);for(n=0;n=0;s--)if(n[s]===t||n[s].listener===t){i=n[s].listener,o=s;break}if(o<0)return this;0===o?n.shift():function(e,t){for(;t+1=0;r--)this.removeListener(e,t[r]);return this},s.prototype.listeners=function(e){return h(this,e,!0)},s.prototype.rawListeners=function(e){return h(this,e,!1)},s.listenerCount=function(e,t){return"function"==typeof e.listenerCount?e.listenerCount(t):f.call(e,t)},s.prototype.listenerCount=f,s.prototype.eventNames=function(){return this._eventsCount>0?t(this._events):[]}},21102:(e,t,n)=>{"use strict";var r=n(46291),o=s(Error);function s(e){return t.displayName=e.displayName||e.name,t;function t(t){return t&&(t=r.apply(null,arguments)),new e(t)}}e.exports=o,o.eval=s(EvalError),o.range=s(RangeError),o.reference=s(ReferenceError),o.syntax=s(SyntaxError),o.type=s(TypeError),o.uri=s(URIError),o.create=s},46291:e=>{!function(){var t;function n(e){for(var t,n,r,o,s=1,i=[].slice.call(arguments),a=0,l=e.length,c="",u=!1,p=!1,h=function(){return i[s++]},f=function(){for(var n="";/\d/.test(e[a]);)n+=e[a++],t=e[a];return n.length>0?parseInt(n):null};a{"use strict";var t=Array.prototype.slice,n=Object.prototype.toString;e.exports=function(e){var r=this;if("function"!=typeof r||"[object Function]"!==n.call(r))throw new TypeError("Function.prototype.bind called on incompatible "+r);for(var o,s=t.call(arguments,1),i=Math.max(0,r.length-s.length),a=[],l=0;l{"use strict";var r=n(17648);e.exports=Function.prototype.bind||r},40210:(e,t,n)=>{"use strict";var r,o=SyntaxError,s=Function,i=TypeError,a=function(e){try{return s('"use strict"; return ('+e+").constructor;")()}catch(e){}},l=Object.getOwnPropertyDescriptor;if(l)try{l({},"")}catch(e){l=null}var c=function(){throw new i},u=l?function(){try{return c}catch(e){try{return l(arguments,"callee").get}catch(e){return c}}}():c,p=n(41405)(),h=n(28185)(),f=Object.getPrototypeOf||(h?function(e){return e.__proto__}:null),d={},m="undefined"!=typeof Uint8Array&&f?f(Uint8Array):r,g={"%AggregateError%":"undefined"==typeof AggregateError?r:AggregateError,"%Array%":Array,"%ArrayBuffer%":"undefined"==typeof ArrayBuffer?r:ArrayBuffer,"%ArrayIteratorPrototype%":p&&f?f([][Symbol.iterator]()):r,"%AsyncFromSyncIteratorPrototype%":r,"%AsyncFunction%":d,"%AsyncGenerator%":d,"%AsyncGeneratorFunction%":d,"%AsyncIteratorPrototype%":d,"%Atomics%":"undefined"==typeof Atomics?r:Atomics,"%BigInt%":"undefined"==typeof BigInt?r:BigInt,"%BigInt64Array%":"undefined"==typeof BigInt64Array?r:BigInt64Array,"%BigUint64Array%":"undefined"==typeof BigUint64Array?r:BigUint64Array,"%Boolean%":Boolean,"%DataView%":"undefined"==typeof DataView?r:DataView,"%Date%":Date,"%decodeURI%":decodeURI,"%decodeURIComponent%":decodeURIComponent,"%encodeURI%":encodeURI,"%encodeURIComponent%":encodeURIComponent,"%Error%":Error,"%eval%":eval,"%EvalError%":EvalError,"%Float32Array%":"undefined"==typeof Float32Array?r:Float32Array,"%Float64Array%":"undefined"==typeof Float64Array?r:Float64Array,"%FinalizationRegistry%":"undefined"==typeof FinalizationRegistry?r:FinalizationRegistry,"%Function%":s,"%GeneratorFunction%":d,"%Int8Array%":"undefined"==typeof Int8Array?r:Int8Array,"%Int16Array%":"undefined"==typeof Int16Array?r:Int16Array,"%Int32Array%":"undefined"==typeof Int32Array?r:Int32Array,"%isFinite%":isFinite,"%isNaN%":isNaN,"%IteratorPrototype%":p&&f?f(f([][Symbol.iterator]())):r,"%JSON%":"object"==typeof JSON?JSON:r,"%Map%":"undefined"==typeof Map?r:Map,"%MapIteratorPrototype%":"undefined"!=typeof Map&&p&&f?f((new Map)[Symbol.iterator]()):r,"%Math%":Math,"%Number%":Number,"%Object%":Object,"%parseFloat%":parseFloat,"%parseInt%":parseInt,"%Promise%":"undefined"==typeof Promise?r:Promise,"%Proxy%":"undefined"==typeof Proxy?r:Proxy,"%RangeError%":RangeError,"%ReferenceError%":ReferenceError,"%Reflect%":"undefined"==typeof Reflect?r:Reflect,"%RegExp%":RegExp,"%Set%":"undefined"==typeof Set?r:Set,"%SetIteratorPrototype%":"undefined"!=typeof Set&&p&&f?f((new Set)[Symbol.iterator]()):r,"%SharedArrayBuffer%":"undefined"==typeof SharedArrayBuffer?r:SharedArrayBuffer,"%String%":String,"%StringIteratorPrototype%":p&&f?f(""[Symbol.iterator]()):r,"%Symbol%":p?Symbol:r,"%SyntaxError%":o,"%ThrowTypeError%":u,"%TypedArray%":m,"%TypeError%":i,"%Uint8Array%":"undefined"==typeof Uint8Array?r:Uint8Array,"%Uint8ClampedArray%":"undefined"==typeof Uint8ClampedArray?r:Uint8ClampedArray,"%Uint16Array%":"undefined"==typeof Uint16Array?r:Uint16Array,"%Uint32Array%":"undefined"==typeof Uint32Array?r:Uint32Array,"%URIError%":URIError,"%WeakMap%":"undefined"==typeof WeakMap?r:WeakMap,"%WeakRef%":"undefined"==typeof WeakRef?r:WeakRef,"%WeakSet%":"undefined"==typeof WeakSet?r:WeakSet};if(f)try{null.error}catch(e){var y=f(f(e));g["%Error.prototype%"]=y}var v=function e(t){var n;if("%AsyncFunction%"===t)n=a("async function () {}");else if("%GeneratorFunction%"===t)n=a("function* () {}");else if("%AsyncGeneratorFunction%"===t)n=a("async function* () {}");else if("%AsyncGenerator%"===t){var r=e("%AsyncGeneratorFunction%");r&&(n=r.prototype)}else if("%AsyncIteratorPrototype%"===t){var o=e("%AsyncGenerator%");o&&f&&(n=f(o.prototype))}return g[t]=n,n},b={"%ArrayBufferPrototype%":["ArrayBuffer","prototype"],"%ArrayPrototype%":["Array","prototype"],"%ArrayProto_entries%":["Array","prototype","entries"],"%ArrayProto_forEach%":["Array","prototype","forEach"],"%ArrayProto_keys%":["Array","prototype","keys"],"%ArrayProto_values%":["Array","prototype","values"],"%AsyncFunctionPrototype%":["AsyncFunction","prototype"],"%AsyncGenerator%":["AsyncGeneratorFunction","prototype"],"%AsyncGeneratorPrototype%":["AsyncGeneratorFunction","prototype","prototype"],"%BooleanPrototype%":["Boolean","prototype"],"%DataViewPrototype%":["DataView","prototype"],"%DatePrototype%":["Date","prototype"],"%ErrorPrototype%":["Error","prototype"],"%EvalErrorPrototype%":["EvalError","prototype"],"%Float32ArrayPrototype%":["Float32Array","prototype"],"%Float64ArrayPrototype%":["Float64Array","prototype"],"%FunctionPrototype%":["Function","prototype"],"%Generator%":["GeneratorFunction","prototype"],"%GeneratorPrototype%":["GeneratorFunction","prototype","prototype"],"%Int8ArrayPrototype%":["Int8Array","prototype"],"%Int16ArrayPrototype%":["Int16Array","prototype"],"%Int32ArrayPrototype%":["Int32Array","prototype"],"%JSONParse%":["JSON","parse"],"%JSONStringify%":["JSON","stringify"],"%MapPrototype%":["Map","prototype"],"%NumberPrototype%":["Number","prototype"],"%ObjectPrototype%":["Object","prototype"],"%ObjProto_toString%":["Object","prototype","toString"],"%ObjProto_valueOf%":["Object","prototype","valueOf"],"%PromisePrototype%":["Promise","prototype"],"%PromiseProto_then%":["Promise","prototype","then"],"%Promise_all%":["Promise","all"],"%Promise_reject%":["Promise","reject"],"%Promise_resolve%":["Promise","resolve"],"%RangeErrorPrototype%":["RangeError","prototype"],"%ReferenceErrorPrototype%":["ReferenceError","prototype"],"%RegExpPrototype%":["RegExp","prototype"],"%SetPrototype%":["Set","prototype"],"%SharedArrayBufferPrototype%":["SharedArrayBuffer","prototype"],"%StringPrototype%":["String","prototype"],"%SymbolPrototype%":["Symbol","prototype"],"%SyntaxErrorPrototype%":["SyntaxError","prototype"],"%TypedArrayPrototype%":["TypedArray","prototype"],"%TypeErrorPrototype%":["TypeError","prototype"],"%Uint8ArrayPrototype%":["Uint8Array","prototype"],"%Uint8ClampedArrayPrototype%":["Uint8ClampedArray","prototype"],"%Uint16ArrayPrototype%":["Uint16Array","prototype"],"%Uint32ArrayPrototype%":["Uint32Array","prototype"],"%URIErrorPrototype%":["URIError","prototype"],"%WeakMapPrototype%":["WeakMap","prototype"],"%WeakSetPrototype%":["WeakSet","prototype"]},w=n(58612),E=n(17642),x=w.call(Function.call,Array.prototype.concat),S=w.call(Function.apply,Array.prototype.splice),_=w.call(Function.call,String.prototype.replace),j=w.call(Function.call,String.prototype.slice),O=w.call(Function.call,RegExp.prototype.exec),k=/[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g,A=/\\(\\)?/g,C=function(e,t){var n,r=e;if(E(b,r)&&(r="%"+(n=b[r])[0]+"%"),E(g,r)){var s=g[r];if(s===d&&(s=v(r)),void 0===s&&!t)throw new i("intrinsic "+e+" exists, but is not available. Please file an issue!");return{alias:n,name:r,value:s}}throw new o("intrinsic "+e+" does not exist!")};e.exports=function(e,t){if("string"!=typeof e||0===e.length)throw new i("intrinsic name must be a non-empty string");if(arguments.length>1&&"boolean"!=typeof t)throw new i('"allowMissing" argument must be a boolean');if(null===O(/^%?[^%]*%?$/,e))throw new o("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var n=function(e){var t=j(e,0,1),n=j(e,-1);if("%"===t&&"%"!==n)throw new o("invalid intrinsic syntax, expected closing `%`");if("%"===n&&"%"!==t)throw new o("invalid intrinsic syntax, expected opening `%`");var r=[];return _(e,k,(function(e,t,n,o){r[r.length]=n?_(o,A,"$1"):t||e})),r}(e),r=n.length>0?n[0]:"",s=C("%"+r+"%",t),a=s.name,c=s.value,u=!1,p=s.alias;p&&(r=p[0],S(n,x([0,1],p)));for(var h=1,f=!0;h=n.length){var v=l(c,d);c=(f=!!v)&&"get"in v&&!("originalValue"in v.get)?v.get:c[d]}else f=E(c,d),c=c[d];f&&!u&&(g[a]=c)}}return c}},28185:e=>{"use strict";var t={foo:{}},n=Object;e.exports=function(){return{__proto__:t}.foo===t.foo&&!({__proto__:null}instanceof n)}},41405:(e,t,n)=>{"use strict";var r="undefined"!=typeof Symbol&&Symbol,o=n(55419);e.exports=function(){return"function"==typeof r&&("function"==typeof Symbol&&("symbol"==typeof r("foo")&&("symbol"==typeof Symbol("bar")&&o())))}},55419:e=>{"use strict";e.exports=function(){if("function"!=typeof Symbol||"function"!=typeof Object.getOwnPropertySymbols)return!1;if("symbol"==typeof Symbol.iterator)return!0;var e={},t=Symbol("test"),n=Object(t);if("string"==typeof t)return!1;if("[object Symbol]"!==Object.prototype.toString.call(t))return!1;if("[object Symbol]"!==Object.prototype.toString.call(n))return!1;for(t in e[t]=42,e)return!1;if("function"==typeof Object.keys&&0!==Object.keys(e).length)return!1;if("function"==typeof Object.getOwnPropertyNames&&0!==Object.getOwnPropertyNames(e).length)return!1;var r=Object.getOwnPropertySymbols(e);if(1!==r.length||r[0]!==t)return!1;if(!Object.prototype.propertyIsEnumerable.call(e,t))return!1;if("function"==typeof Object.getOwnPropertyDescriptor){var o=Object.getOwnPropertyDescriptor(e,t);if(42!==o.value||!0!==o.enumerable)return!1}return!0}},17642:(e,t,n)=>{"use strict";var r=n(58612);e.exports=r.call(Function.call,Object.prototype.hasOwnProperty)},47802:e=>{function t(e){return e instanceof Map?e.clear=e.delete=e.set=function(){throw new Error("map is read-only")}:e instanceof Set&&(e.add=e.clear=e.delete=function(){throw new Error("set is read-only")}),Object.freeze(e),Object.getOwnPropertyNames(e).forEach((function(n){var r=e[n];"object"!=typeof r||Object.isFrozen(r)||t(r)})),e}var n=t,r=t;n.default=r;class o{constructor(e){void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1}ignoreMatch(){this.isMatchIgnored=!0}}function s(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function i(e,...t){const n=Object.create(null);for(const t in e)n[t]=e[t];return t.forEach((function(e){for(const t in e)n[t]=e[t]})),n}const a=e=>!!e.kind;class l{constructor(e,t){this.buffer="",this.classPrefix=t.classPrefix,e.walk(this)}addText(e){this.buffer+=s(e)}openNode(e){if(!a(e))return;let t=e.kind;e.sublanguage||(t=`${this.classPrefix}${t}`),this.span(t)}closeNode(e){a(e)&&(this.buffer+="")}value(){return this.buffer}span(e){this.buffer+=``}}class c{constructor(){this.rootNode={children:[]},this.stack=[this.rootNode]}get top(){return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){this.top.children.push(e)}openNode(e){const t={kind:e,children:[]};this.add(t),this.stack.push(t)}closeNode(){if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,t){return"string"==typeof t?e.addText(t):t.children&&(e.openNode(t),t.children.forEach((t=>this._walk(e,t))),e.closeNode(t)),e}static _collapse(e){"string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{c._collapse(e)})))}}class u extends c{constructor(e){super(),this.options=e}addKeyword(e,t){""!==e&&(this.openNode(t),this.addText(e),this.closeNode())}addText(e){""!==e&&this.add(e)}addSublanguage(e,t){const n=e.root;n.kind=t,n.sublanguage=!0,this.add(n)}toHTML(){return new l(this,this.options).value()}finalize(){return!0}}function p(e){return e?"string"==typeof e?e:e.source:null}const h=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;const f="[a-zA-Z]\\w*",d="[a-zA-Z_]\\w*",m="\\b\\d+(\\.\\d+)?",g="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",y="\\b(0b[01]+)",v={begin:"\\\\[\\s\\S]",relevance:0},b={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[v]},w={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[v]},E={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},x=function(e,t,n={}){const r=i({className:"comment",begin:e,end:t,contains:[]},n);return r.contains.push(E),r.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):",relevance:0}),r},S=x("//","$"),_=x("/\\*","\\*/"),j=x("#","$"),O={className:"number",begin:m,relevance:0},k={className:"number",begin:g,relevance:0},A={className:"number",begin:y,relevance:0},C={className:"number",begin:m+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0},P={begin:/(?=\/[^/\n]*\/)/,contains:[{className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[v,{begin:/\[/,end:/\]/,relevance:0,contains:[v]}]}]},N={className:"title",begin:f,relevance:0},I={className:"title",begin:d,relevance:0},T={begin:"\\.\\s*"+d,relevance:0};var R=Object.freeze({__proto__:null,MATCH_NOTHING_RE:/\b\B/,IDENT_RE:f,UNDERSCORE_IDENT_RE:d,NUMBER_RE:m,C_NUMBER_RE:g,BINARY_NUMBER_RE:y,RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",SHEBANG:(e={})=>{const t=/^#![ ]*\//;return e.binary&&(e.begin=function(...e){return e.map((e=>p(e))).join("")}(t,/.*\b/,e.binary,/\b.*/)),i({className:"meta",begin:t,end:/$/,relevance:0,"on:begin":(e,t)=>{0!==e.index&&t.ignoreMatch()}},e)},BACKSLASH_ESCAPE:v,APOS_STRING_MODE:b,QUOTE_STRING_MODE:w,PHRASAL_WORDS_MODE:E,COMMENT:x,C_LINE_COMMENT_MODE:S,C_BLOCK_COMMENT_MODE:_,HASH_COMMENT_MODE:j,NUMBER_MODE:O,C_NUMBER_MODE:k,BINARY_NUMBER_MODE:A,CSS_NUMBER_MODE:C,REGEXP_MODE:P,TITLE_MODE:N,UNDERSCORE_TITLE_MODE:I,METHOD_GUARD:T,END_SAME_AS_BEGIN:function(e){return Object.assign(e,{"on:begin":(e,t)=>{t.data._beginMatch=e[1]},"on:end":(e,t)=>{t.data._beginMatch!==e[1]&&t.ignoreMatch()}})}});function M(e,t){"."===e.input[e.index-1]&&t.ignoreMatch()}function D(e,t){t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",e.__beforeBegin=M,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords,void 0===e.relevance&&(e.relevance=0))}function F(e,t){Array.isArray(e.illegal)&&(e.illegal=function(...e){return"("+e.map((e=>p(e))).join("|")+")"}(...e.illegal))}function L(e,t){if(e.match){if(e.begin||e.end)throw new Error("begin & end are not supported with match");e.begin=e.match,delete e.match}}function B(e,t){void 0===e.relevance&&(e.relevance=1)}const $=["of","and","for","in","not","or","if","then","parent","list","value"],q="keyword";function U(e,t,n=q){const r={};return"string"==typeof e?o(n,e.split(" ")):Array.isArray(e)?o(n,e):Object.keys(e).forEach((function(n){Object.assign(r,U(e[n],t,n))})),r;function o(e,n){t&&(n=n.map((e=>e.toLowerCase()))),n.forEach((function(t){const n=t.split("|");r[n[0]]=[e,z(n[0],n[1])]}))}}function z(e,t){return t?Number(t):function(e){return $.includes(e.toLowerCase())}(e)?0:1}function V(e,{plugins:t}){function n(t,n){return new RegExp(p(t),"m"+(e.case_insensitive?"i":"")+(n?"g":""))}class r{constructor(){this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}addRule(e,t){t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]),this.matchAt+=function(e){return new RegExp(e.toString()+"|").exec("").length-1}(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null);const e=this.regexes.map((e=>e[1]));this.matcherRe=n(function(e,t="|"){let n=0;return e.map((e=>{n+=1;const t=n;let r=p(e),o="";for(;r.length>0;){const e=h.exec(r);if(!e){o+=r;break}o+=r.substring(0,e.index),r=r.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?o+="\\"+String(Number(e[1])+t):(o+=e[0],"("===e[0]&&n++)}return o})).map((e=>`(${e})`)).join(t)}(e),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex;const t=this.matcherRe.exec(e);if(!t)return null;const n=t.findIndex(((e,t)=>t>0&&void 0!==e)),r=this.matchIndexes[n];return t.splice(0,n),Object.assign(t,r)}}class o{constructor(){this.rules=[],this.multiRegexes=[],this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){if(this.multiRegexes[e])return this.multiRegexes[e];const t=new r;return this.rules.slice(e).forEach((([e,n])=>t.addRule(e,n))),t.compile(),this.multiRegexes[e]=t,t}resumingScanAtSamePosition(){return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,t){this.rules.push([e,t]),"begin"===t.type&&this.count++}exec(e){const t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex;let n=t.exec(e);if(this.resumingScanAtSamePosition())if(n&&n.index===this.lastIndex);else{const t=this.getMatcher(0);t.lastIndex=this.lastIndex+1,n=t.exec(e)}return n&&(this.regexIndex+=n.position+1,this.regexIndex===this.count&&this.considerAll()),n}}if(e.compilerExtensions||(e.compilerExtensions=[]),e.contains&&e.contains.includes("self"))throw new Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");return e.classNameAliases=i(e.classNameAliases||{}),function t(r,s){const a=r;if(r.isCompiled)return a;[L].forEach((e=>e(r,s))),e.compilerExtensions.forEach((e=>e(r,s))),r.__beforeBegin=null,[D,F,B].forEach((e=>e(r,s))),r.isCompiled=!0;let l=null;if("object"==typeof r.keywords&&(l=r.keywords.$pattern,delete r.keywords.$pattern),r.keywords&&(r.keywords=U(r.keywords,e.case_insensitive)),r.lexemes&&l)throw new Error("ERR: Prefer `keywords.$pattern` to `mode.lexemes`, BOTH are not allowed. (see mode reference) ");return l=l||r.lexemes||/\w+/,a.keywordPatternRe=n(l,!0),s&&(r.begin||(r.begin=/\B|\b/),a.beginRe=n(r.begin),r.endSameAsBegin&&(r.end=r.begin),r.end||r.endsWithParent||(r.end=/\B|\b/),r.end&&(a.endRe=n(r.end)),a.terminatorEnd=p(r.end)||"",r.endsWithParent&&s.terminatorEnd&&(a.terminatorEnd+=(r.end?"|":"")+s.terminatorEnd)),r.illegal&&(a.illegalRe=n(r.illegal)),r.contains||(r.contains=[]),r.contains=[].concat(...r.contains.map((function(e){return function(e){e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((function(t){return i(e,{variants:null},t)})));if(e.cachedVariants)return e.cachedVariants;if(W(e))return i(e,{starts:e.starts?i(e.starts):null});if(Object.isFrozen(e))return i(e);return e}("self"===e?r:e)}))),r.contains.forEach((function(e){t(e,a)})),r.starts&&t(r.starts,s),a.matcher=function(e){const t=new o;return e.contains.forEach((e=>t.addRule(e.begin,{rule:e,type:"begin"}))),e.terminatorEnd&&t.addRule(e.terminatorEnd,{type:"end"}),e.illegal&&t.addRule(e.illegal,{type:"illegal"}),t}(a),a}(e)}function W(e){return!!e&&(e.endsWithParent||W(e.starts))}function J(e){const t={props:["language","code","autodetect"],data:function(){return{detectedLanguage:"",unknownLanguage:!1}},computed:{className(){return this.unknownLanguage?"":"hljs "+this.detectedLanguage},highlighted(){if(!this.autoDetect&&!e.getLanguage(this.language))return console.warn(`The language "${this.language}" you specified could not be found.`),this.unknownLanguage=!0,s(this.code);let t={};return this.autoDetect?(t=e.highlightAuto(this.code),this.detectedLanguage=t.language):(t=e.highlight(this.language,this.code,this.ignoreIllegals),this.detectedLanguage=this.language),t.value},autoDetect(){return!this.language||(e=this.autodetect,Boolean(e||""===e));var e},ignoreIllegals:()=>!0},render(e){return e("pre",{},[e("code",{class:this.className,domProps:{innerHTML:this.highlighted}})])}};return{Component:t,VuePlugin:{install(e){e.component("highlightjs",t)}}}}const K={"after:highlightElement":({el:e,result:t,text:n})=>{const r=G(e);if(!r.length)return;const o=document.createElement("div");o.innerHTML=t.value,t.value=function(e,t,n){let r=0,o="";const i=[];function a(){return e.length&&t.length?e[0].offset!==t[0].offset?e[0].offset"}function c(e){o+=""}function u(e){("start"===e.event?l:c)(e.node)}for(;e.length||t.length;){let t=a();if(o+=s(n.substring(r,t[0].offset)),r=t[0].offset,t===e){i.reverse().forEach(c);do{u(t.splice(0,1)[0]),t=a()}while(t===e&&t.length&&t[0].offset===r);i.reverse().forEach(l)}else"start"===t[0].event?i.push(t[0].node):i.pop(),u(t.splice(0,1)[0])}return o+s(n.substr(r))}(r,G(o),n)}};function H(e){return e.nodeName.toLowerCase()}function G(e){const t=[];return function e(n,r){for(let o=n.firstChild;o;o=o.nextSibling)3===o.nodeType?r+=o.nodeValue.length:1===o.nodeType&&(t.push({event:"start",offset:r,node:o}),r=e(o,r),H(o).match(/br|hr|img|input/)||t.push({event:"stop",offset:r,node:o}));return r}(e,0),t}const Z={},Y=e=>{console.error(e)},X=(e,...t)=>{console.log(`WARN: ${e}`,...t)},Q=(e,t)=>{Z[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),Z[`${e}/${t}`]=!0)},ee=s,te=i,ne=Symbol("nomatch");var re=function(e){const t=Object.create(null),r=Object.create(null),s=[];let i=!0;const a=/(^(<[^>]+>|\t|)+|\n)/gm,l="Could not find the language '{}', did you forget to load/include a language module?",c={disableAutodetect:!0,name:"Plain text",contains:[]};let p={noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:null,__emitter:u};function h(e){return p.noHighlightRe.test(e)}function f(e,t,n,r){let o="",s="";"object"==typeof t?(o=e,n=t.ignoreIllegals,s=t.language,r=void 0):(Q("10.7.0","highlight(lang, code, ...args) has been deprecated."),Q("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"),s=e,o=t);const i={code:o,language:s};O("before:highlight",i);const a=i.result?i.result:d(i.language,i.code,n,r);return a.code=i.code,O("after:highlight",a),a}function d(e,n,r,a){function c(e,t){const n=E.case_insensitive?t[0].toLowerCase():t[0];return Object.prototype.hasOwnProperty.call(e.keywords,n)&&e.keywords[n]}function u(){null!=j.subLanguage?function(){if(""===A)return;let e=null;if("string"==typeof j.subLanguage){if(!t[j.subLanguage])return void k.addText(A);e=d(j.subLanguage,A,!0,O[j.subLanguage]),O[j.subLanguage]=e.top}else e=m(A,j.subLanguage.length?j.subLanguage:null);j.relevance>0&&(C+=e.relevance),k.addSublanguage(e.emitter,e.language)}():function(){if(!j.keywords)return void k.addText(A);let e=0;j.keywordPatternRe.lastIndex=0;let t=j.keywordPatternRe.exec(A),n="";for(;t;){n+=A.substring(e,t.index);const r=c(j,t);if(r){const[e,o]=r;if(k.addText(n),n="",C+=o,e.startsWith("_"))n+=t[0];else{const n=E.classNameAliases[e]||e;k.addKeyword(t[0],n)}}else n+=t[0];e=j.keywordPatternRe.lastIndex,t=j.keywordPatternRe.exec(A)}n+=A.substr(e),k.addText(n)}(),A=""}function h(e){return e.className&&k.openNode(E.classNameAliases[e.className]||e.className),j=Object.create(e,{parent:{value:j}}),j}function f(e,t,n){let r=function(e,t){const n=e&&e.exec(t);return n&&0===n.index}(e.endRe,n);if(r){if(e["on:end"]){const n=new o(e);e["on:end"](t,n),n.isMatchIgnored&&(r=!1)}if(r){for(;e.endsParent&&e.parent;)e=e.parent;return e}}if(e.endsWithParent)return f(e.parent,t,n)}function g(e){return 0===j.matcher.regexIndex?(A+=e[0],1):(I=!0,0)}function y(e){const t=e[0],n=e.rule,r=new o(n),s=[n.__beforeBegin,n["on:begin"]];for(const n of s)if(n&&(n(e,r),r.isMatchIgnored))return g(t);return n&&n.endSameAsBegin&&(n.endRe=new RegExp(t.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),"m")),n.skip?A+=t:(n.excludeBegin&&(A+=t),u(),n.returnBegin||n.excludeBegin||(A=t)),h(n),n.returnBegin?0:t.length}function v(e){const t=e[0],r=n.substr(e.index),o=f(j,e,r);if(!o)return ne;const s=j;s.skip?A+=t:(s.returnEnd||s.excludeEnd||(A+=t),u(),s.excludeEnd&&(A=t));do{j.className&&k.closeNode(),j.skip||j.subLanguage||(C+=j.relevance),j=j.parent}while(j!==o.parent);return o.starts&&(o.endSameAsBegin&&(o.starts.endRe=o.endRe),h(o.starts)),s.returnEnd?0:t.length}let b={};function w(t,o){const s=o&&o[0];if(A+=t,null==s)return u(),0;if("begin"===b.type&&"end"===o.type&&b.index===o.index&&""===s){if(A+=n.slice(o.index,o.index+1),!i){const t=new Error("0 width match regex");throw t.languageName=e,t.badRule=b.rule,t}return 1}if(b=o,"begin"===o.type)return y(o);if("illegal"===o.type&&!r){const e=new Error('Illegal lexeme "'+s+'" for mode "'+(j.className||"")+'"');throw e.mode=j,e}if("end"===o.type){const e=v(o);if(e!==ne)return e}if("illegal"===o.type&&""===s)return 1;if(N>1e5&&N>3*o.index){throw new Error("potential infinite loop, way more iterations than matches")}return A+=s,s.length}const E=S(e);if(!E)throw Y(l.replace("{}",e)),new Error('Unknown language: "'+e+'"');const x=V(E,{plugins:s});let _="",j=a||x;const O={},k=new p.__emitter(p);!function(){const e=[];for(let t=j;t!==E;t=t.parent)t.className&&e.unshift(t.className);e.forEach((e=>k.openNode(e)))}();let A="",C=0,P=0,N=0,I=!1;try{for(j.matcher.considerAll();;){N++,I?I=!1:j.matcher.considerAll(),j.matcher.lastIndex=P;const e=j.matcher.exec(n);if(!e)break;const t=w(n.substring(P,e.index),e);P=e.index+t}return w(n.substr(P)),k.closeAllNodes(),k.finalize(),_=k.toHTML(),{relevance:Math.floor(C),value:_,language:e,illegal:!1,emitter:k,top:j}}catch(t){if(t.message&&t.message.includes("Illegal"))return{illegal:!0,illegalBy:{msg:t.message,context:n.slice(P-100,P+100),mode:t.mode},sofar:_,relevance:0,value:ee(n),emitter:k};if(i)return{illegal:!1,relevance:0,value:ee(n),emitter:k,language:e,top:j,errorRaised:t};throw t}}function m(e,n){n=n||p.languages||Object.keys(t);const r=function(e){const t={relevance:0,emitter:new p.__emitter(p),value:ee(e),illegal:!1,top:c};return t.emitter.addText(e),t}(e),o=n.filter(S).filter(j).map((t=>d(t,e,!1)));o.unshift(r);const s=o.sort(((e,t)=>{if(e.relevance!==t.relevance)return t.relevance-e.relevance;if(e.language&&t.language){if(S(e.language).supersetOf===t.language)return 1;if(S(t.language).supersetOf===e.language)return-1}return 0})),[i,a]=s,l=i;return l.second_best=a,l}const g={"before:highlightElement":({el:e})=>{p.useBR&&(e.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n"))},"after:highlightElement":({result:e})=>{p.useBR&&(e.value=e.value.replace(/\n/g,"
"))}},y=/^(<[^>]+>|\t)+/gm,v={"after:highlightElement":({result:e})=>{p.tabReplace&&(e.value=e.value.replace(y,(e=>e.replace(/\t/g,p.tabReplace))))}};function b(e){let t=null;const n=function(e){let t=e.className+" ";t+=e.parentNode?e.parentNode.className:"";const n=p.languageDetectRe.exec(t);if(n){const t=S(n[1]);return t||(X(l.replace("{}",n[1])),X("Falling back to no-highlight mode for this block.",e)),t?n[1]:"no-highlight"}return t.split(/\s+/).find((e=>h(e)||S(e)))}(e);if(h(n))return;O("before:highlightElement",{el:e,language:n}),t=e;const o=t.textContent,s=n?f(o,{language:n,ignoreIllegals:!0}):m(o);O("after:highlightElement",{el:e,result:s,text:o}),e.innerHTML=s.value,function(e,t,n){const o=t?r[t]:n;e.classList.add("hljs"),o&&e.classList.add(o)}(e,n,s.language),e.result={language:s.language,re:s.relevance,relavance:s.relevance},s.second_best&&(e.second_best={language:s.second_best.language,re:s.second_best.relevance,relavance:s.second_best.relevance})}const w=()=>{if(w.called)return;w.called=!0,Q("10.6.0","initHighlighting() is deprecated. Use highlightAll() instead.");document.querySelectorAll("pre code").forEach(b)};let E=!1;function x(){if("loading"===document.readyState)return void(E=!0);document.querySelectorAll("pre code").forEach(b)}function S(e){return e=(e||"").toLowerCase(),t[e]||t[r[e]]}function _(e,{languageName:t}){"string"==typeof e&&(e=[e]),e.forEach((e=>{r[e.toLowerCase()]=t}))}function j(e){const t=S(e);return t&&!t.disableAutodetect}function O(e,t){const n=e;s.forEach((function(e){e[n]&&e[n](t)}))}"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(function(){E&&x()}),!1),Object.assign(e,{highlight:f,highlightAuto:m,highlightAll:x,fixMarkup:function(e){return Q("10.2.0","fixMarkup will be removed entirely in v11.0"),Q("10.2.0","Please see https://github.com/highlightjs/highlight.js/issues/2534"),t=e,p.tabReplace||p.useBR?t.replace(a,(e=>"\n"===e?p.useBR?"
":e:p.tabReplace?e.replace(/\t/g,p.tabReplace):e)):t;var t},highlightElement:b,highlightBlock:function(e){return Q("10.7.0","highlightBlock will be removed entirely in v12.0"),Q("10.7.0","Please use highlightElement now."),b(e)},configure:function(e){e.useBR&&(Q("10.3.0","'useBR' will be removed entirely in v11.0"),Q("10.3.0","Please see https://github.com/highlightjs/highlight.js/issues/2559")),p=te(p,e)},initHighlighting:w,initHighlightingOnLoad:function(){Q("10.6.0","initHighlightingOnLoad() is deprecated. Use highlightAll() instead."),E=!0},registerLanguage:function(n,r){let o=null;try{o=r(e)}catch(e){if(Y("Language definition for '{}' could not be registered.".replace("{}",n)),!i)throw e;Y(e),o=c}o.name||(o.name=n),t[n]=o,o.rawDefinition=r.bind(null,e),o.aliases&&_(o.aliases,{languageName:n})},unregisterLanguage:function(e){delete t[e];for(const t of Object.keys(r))r[t]===e&&delete r[t]},listLanguages:function(){return Object.keys(t)},getLanguage:S,registerAliases:_,requireLanguage:function(e){Q("10.4.0","requireLanguage will be removed entirely in v11."),Q("10.4.0","Please see https://github.com/highlightjs/highlight.js/pull/2844");const t=S(e);if(t)return t;throw new Error("The '{}' language is required, but not loaded.".replace("{}",e))},autoDetection:j,inherit:te,addPlugin:function(e){!function(e){e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=t=>{e["before:highlightBlock"](Object.assign({block:t.el},t))}),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=t=>{e["after:highlightBlock"](Object.assign({block:t.el},t))})}(e),s.push(e)},vuePlugin:J(e).VuePlugin}),e.debugMode=function(){i=!1},e.safeMode=function(){i=!0},e.versionString="10.7.3";for(const e in R)"object"==typeof R[e]&&n(R[e]);return Object.assign(e,R),e.addPlugin(g),e.addPlugin(K),e.addPlugin(v),e}({});e.exports=re},61519:e=>{function t(...e){return e.map((e=>{return(t=e)?"string"==typeof t?t:t.source:null;var t})).join("")}e.exports=function(e){const n={},r={begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[n]}]};Object.assign(n,{className:"variable",variants:[{begin:t(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},r]});const o={className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]},s={begin:/<<-?\s*(?=\w+)/,starts:{contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,className:"string"})]}},i={className:"string",begin:/"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,n,o]};o.contains.push(i);const a={begin:/\$\(\(/,end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,n]},l=e.SHEBANG({binary:`(${["fish","bash","zsh","sh","csh","ksh","tcsh","dash","scsh"].join("|")})`,relevance:10}),c={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{name:"Bash",aliases:["sh","zsh"],keywords:{$pattern:/\b[a-z._-]+\b/,keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp"},contains:[l,e.SHEBANG(),c,a,e.HASH_COMMENT_MODE,s,i,{className:"",begin:/\\"/},{className:"string",begin:/'/,end:/'/},n]}}},30786:e=>{function t(...e){return e.map((e=>{return(t=e)?"string"==typeof t?t:t.source:null;var t})).join("")}e.exports=function(e){const n="HTTP/(2|1\\.[01])",r={className:"attribute",begin:t("^",/[A-Za-z][A-Za-z0-9-]*/,"(?=\\:\\s)"),starts:{contains:[{className:"punctuation",begin:/: /,relevance:0,starts:{end:"$",relevance:0}}]}},o=[r,{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0}}];return{name:"HTTP",aliases:["https"],illegal:/\S/,contains:[{begin:"^(?="+n+" \\d{3})",end:/$/,contains:[{className:"meta",begin:n},{className:"number",begin:"\\b\\d{3}\\b"}],starts:{end:/\b\B/,illegal:/\S/,contains:o}},{begin:"(?=^[A-Z]+ (.*?) "+n+"$)",end:/$/,contains:[{className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{className:"meta",begin:n},{className:"keyword",begin:"[A-Z]+"}],starts:{end:/\b\B/,illegal:/\S/,contains:o}},e.inherit(r,{relevance:0})]}}},96344:e=>{const t="[A-Za-z$_][0-9A-Za-z$_]*",n=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],r=["true","false","null","undefined","NaN","Infinity"],o=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer","BigInt64Array","BigUint64Array","BigInt"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"]);function s(e){return i("(?=",e,")")}function i(...e){return e.map((e=>{return(t=e)?"string"==typeof t?t:t.source:null;var t})).join("")}e.exports=function(e){const a=t,l="<>",c="",u={begin:/<[A-Za-z0-9\\._:-]+/,end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,t)=>{const n=e[0].length+e.index,r=e.input[n];"<"!==r?">"===r&&(((e,{after:t})=>{const n="",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:e.UNDERSCORE_IDENT_RE,relevance:0},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:p,contains:S}]}]},{begin:/,/,relevance:0},{className:"",begin:/\s/,end:/\s*/,skip:!0},{variants:[{begin:l,end:c},{begin:u.begin,"on:begin":u.isTrulyOpeningTag,end:u.end}],subLanguage:"xml",contains:[{begin:u.begin,end:u.end,skip:!0,contains:["self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/[{;]/,excludeEnd:!0,keywords:p,contains:["self",e.inherit(e.TITLE_MODE,{begin:a}),_],illegal:/%/},{beginKeywords:"while if switch catch for"},{className:"function",begin:e.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{",returnBegin:!0,contains:[_,e.inherit(e.TITLE_MODE,{begin:a})]},{variants:[{begin:"\\."+a},{begin:"\\$"+a}],relevance:0},{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"[\]]/,contains:[{beginKeywords:"extends"},e.UNDERSCORE_TITLE_MODE]},{begin:/\b(?=constructor)/,end:/[{;]/,excludeEnd:!0,contains:[e.inherit(e.TITLE_MODE,{begin:a}),"self",_]},{begin:"(get|set)\\s+(?="+a+"\\()",end:/\{/,keywords:"get set",contains:[e.inherit(e.TITLE_MODE,{begin:a}),{begin:/\(\)/},_]},{begin:/\$[(.]/}]}}},82026:e=>{e.exports=function(e){const t={literal:"true false null"},n=[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE],r=[e.QUOTE_STRING_MODE,e.C_NUMBER_MODE],o={end:",",endsWithParent:!0,excludeEnd:!0,contains:r,keywords:t},s={begin:/\{/,end:/\}/,contains:[{className:"attr",begin:/"/,end:/"/,contains:[e.BACKSLASH_ESCAPE],illegal:"\\n"},e.inherit(o,{begin:/:/})].concat(n),illegal:"\\S"},i={begin:"\\[",end:"\\]",contains:[e.inherit(o)],illegal:"\\S"};return r.push(s,i),n.forEach((function(e){r.push(e)})),{name:"JSON",contains:r,keywords:t,illegal:"\\S"}}},66336:e=>{e.exports=function(e){const t={$pattern:/-?[A-z\.\-]+\b/,keyword:"if else foreach return do while until elseif begin for trap data dynamicparam end break throw param continue finally in switch exit filter try process catch hidden static parameter",built_in:"ac asnp cat cd CFS chdir clc clear clhy cli clp cls clv cnsn compare copy cp cpi cpp curl cvpa dbp del diff dir dnsn ebp echo|0 epal epcsv epsn erase etsn exsn fc fhx fl ft fw gal gbp gc gcb gci gcm gcs gdr gerr ghy gi gin gjb gl gm gmo gp gps gpv group gsn gsnp gsv gtz gu gv gwmi h history icm iex ihy ii ipal ipcsv ipmo ipsn irm ise iwmi iwr kill lp ls man md measure mi mount move mp mv nal ndr ni nmo npssc nsn nv ogv oh popd ps pushd pwd r rbp rcjb rcsn rd rdr ren ri rjb rm rmdir rmo rni rnp rp rsn rsnp rujb rv rvpa rwmi sajb sal saps sasv sbp sc scb select set shcm si sl sleep sls sort sp spjb spps spsv start stz sujb sv swmi tee trcm type wget where wjb write"},n={begin:"`[\\s\\S]",relevance:0},r={className:"variable",variants:[{begin:/\$\B/},{className:"keyword",begin:/\$this/},{begin:/\$[\w\d][\w\d_:]*/}]},o={className:"string",variants:[{begin:/"/,end:/"/},{begin:/@"/,end:/^"@/}],contains:[n,r,{className:"variable",begin:/\$[A-z]/,end:/[^A-z]/}]},s={className:"string",variants:[{begin:/'/,end:/'/},{begin:/@'/,end:/^'@/}]},i=e.inherit(e.COMMENT(null,null),{variants:[{begin:/#/,end:/$/},{begin:/<#/,end:/#>/}],contains:[{className:"doctag",variants:[{begin:/\.(synopsis|description|example|inputs|outputs|notes|link|component|role|functionality)/},{begin:/\.(parameter|forwardhelptargetname|forwardhelpcategory|remotehelprunspace|externalhelp)\s+\S+/}]}]}),a={className:"built_in",variants:[{begin:"(".concat("Add|Clear|Close|Copy|Enter|Exit|Find|Format|Get|Hide|Join|Lock|Move|New|Open|Optimize|Pop|Push|Redo|Remove|Rename|Reset|Resize|Search|Select|Set|Show|Skip|Split|Step|Switch|Undo|Unlock|Watch|Backup|Checkpoint|Compare|Compress|Convert|ConvertFrom|ConvertTo|Dismount|Edit|Expand|Export|Group|Import|Initialize|Limit|Merge|Mount|Out|Publish|Restore|Save|Sync|Unpublish|Update|Approve|Assert|Build|Complete|Confirm|Deny|Deploy|Disable|Enable|Install|Invoke|Register|Request|Restart|Resume|Start|Stop|Submit|Suspend|Uninstall|Unregister|Wait|Debug|Measure|Ping|Repair|Resolve|Test|Trace|Connect|Disconnect|Read|Receive|Send|Write|Block|Grant|Protect|Revoke|Unblock|Unprotect|Use|ForEach|Sort|Tee|Where",")+(-)[\\w\\d]+")}]},l={className:"class",beginKeywords:"class enum",end:/\s*[{]/,excludeEnd:!0,relevance:0,contains:[e.TITLE_MODE]},c={className:"function",begin:/function\s+/,end:/\s*\{|$/,excludeEnd:!0,returnBegin:!0,relevance:0,contains:[{begin:"function",relevance:0,className:"keyword"},{className:"title",begin:/\w[\w\d]*((-)[\w\d]+)*/,relevance:0},{begin:/\(/,end:/\)/,className:"params",relevance:0,contains:[r]}]},u={begin:/using\s/,end:/$/,returnBegin:!0,contains:[o,s,{className:"keyword",begin:/(using|assembly|command|module|namespace|type)/}]},p={variants:[{className:"operator",begin:"(".concat("-and|-as|-band|-bnot|-bor|-bxor|-casesensitive|-ccontains|-ceq|-cge|-cgt|-cle|-clike|-clt|-cmatch|-cne|-cnotcontains|-cnotlike|-cnotmatch|-contains|-creplace|-csplit|-eq|-exact|-f|-file|-ge|-gt|-icontains|-ieq|-ige|-igt|-ile|-ilike|-ilt|-imatch|-in|-ine|-inotcontains|-inotlike|-inotmatch|-ireplace|-is|-isnot|-isplit|-join|-le|-like|-lt|-match|-ne|-not|-notcontains|-notin|-notlike|-notmatch|-or|-regex|-replace|-shl|-shr|-split|-wildcard|-xor",")\\b")},{className:"literal",begin:/(-)[\w\d]+/,relevance:0}]},h={className:"function",begin:/\[.*\]\s*[\w]+[ ]??\(/,end:/$/,returnBegin:!0,relevance:0,contains:[{className:"keyword",begin:"(".concat(t.keyword.toString().replace(/\s/g,"|"),")\\b"),endsParent:!0,relevance:0},e.inherit(e.TITLE_MODE,{endsParent:!0})]},f=[h,i,n,e.NUMBER_MODE,o,s,a,r,{className:"literal",begin:/\$(null|true|false)\b/},{className:"selector-tag",begin:/@\B/,relevance:0}],d={begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[].concat("self",f,{begin:"("+["string","char","byte","int","long","bool","decimal","single","double","DateTime","xml","array","hashtable","void"].join("|")+")",className:"built_in",relevance:0},{className:"type",begin:/[\.\w\d]+/,relevance:0})};return h.contains.unshift(d),{name:"PowerShell",aliases:["ps","ps1"],case_insensitive:!0,keywords:t,contains:f.concat(l,c,u,p,d)}}},42157:e=>{function t(e){return e?"string"==typeof e?e:e.source:null}function n(e){return r("(?=",e,")")}function r(...e){return e.map((e=>t(e))).join("")}function o(...e){return"("+e.map((e=>t(e))).join("|")+")"}e.exports=function(e){const t=r(/[A-Z_]/,r("(",/[A-Z0-9_.-]*:/,")?"),/[A-Z0-9_.-]*/),s={className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},i={begin:/\s/,contains:[{className:"meta-keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}]},a=e.inherit(i,{begin:/\(/,end:/\)/}),l=e.inherit(e.APOS_STRING_MODE,{className:"meta-string"}),c=e.inherit(e.QUOTE_STRING_MODE,{className:"meta-string"}),u={endsWithParent:!0,illegal:/`]+/}]}]}]};return{name:"HTML, XML",aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],case_insensitive:!0,contains:[{className:"meta",begin://,relevance:10,contains:[i,c,l,a,{begin:/\[/,end:/\]/,contains:[{className:"meta",begin://,contains:[i,a,c,l]}]}]},e.COMMENT(//,{relevance:10}),{begin://,relevance:10},s,{className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",begin:/)/,end:/>/,keywords:{name:"style"},contains:[u],starts:{end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:/)/,end:/>/,keywords:{name:"script"},contains:[u],starts:{end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{className:"tag",begin:/<>|<\/>/},{className:"tag",begin:r(//,/>/,/\s/)))),end:/\/?>/,contains:[{className:"name",begin:t,relevance:0,starts:u}]},{className:"tag",begin:r(/<\//,n(r(t,/>/))),contains:[{className:"name",begin:t,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]}}},54587:e=>{e.exports=function(e){var t="true false yes no null",n="[\\w#;/?:@&=+$,.~*'()[\\]]+",r={className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,{className:"template-variable",variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},o=e.inherit(r,{variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),s={className:"number",begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b"},i={end:",",endsWithParent:!0,excludeEnd:!0,keywords:t,relevance:0},a={begin:/\{/,end:/\}/,contains:[i],illegal:"\\n",relevance:0},l={begin:"\\[",end:"\\]",contains:[i],illegal:"\\n",relevance:0},c=[{className:"attr",variants:[{begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$",relevance:10},{className:"string",begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!\\w+!"+n},{className:"type",begin:"!<"+n+">"},{className:"type",begin:"!"+n},{className:"type",begin:"!!"+n},{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta",begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)",relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:t,keywords:{literal:t}},s,{className:"number",begin:e.C_NUMBER_RE+"\\b",relevance:0},a,l,r],u=[...c];return u.pop(),u.push(o),i.contains=u,{name:"YAML",case_insensitive:!0,aliases:["yml"],contains:c}}},8679:(e,t,n)=>{"use strict";var r=n(59864),o={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},s={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},i={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},a={};function l(e){return r.isMemo(e)?i:a[e.$$typeof]||o}a[r.ForwardRef]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},a[r.Memo]=i;var c=Object.defineProperty,u=Object.getOwnPropertyNames,p=Object.getOwnPropertySymbols,h=Object.getOwnPropertyDescriptor,f=Object.getPrototypeOf,d=Object.prototype;e.exports=function e(t,n,r){if("string"!=typeof n){if(d){var o=f(n);o&&o!==d&&e(t,o,r)}var i=u(n);p&&(i=i.concat(p(n)));for(var a=l(t),m=l(n),g=0;g{t.read=function(e,t,n,r,o){var s,i,a=8*o-r-1,l=(1<>1,u=-7,p=n?o-1:0,h=n?-1:1,f=e[t+p];for(p+=h,s=f&(1<<-u)-1,f>>=-u,u+=a;u>0;s=256*s+e[t+p],p+=h,u-=8);for(i=s&(1<<-u)-1,s>>=-u,u+=r;u>0;i=256*i+e[t+p],p+=h,u-=8);if(0===s)s=1-c;else{if(s===l)return i?NaN:1/0*(f?-1:1);i+=Math.pow(2,r),s-=c}return(f?-1:1)*i*Math.pow(2,s-r)},t.write=function(e,t,n,r,o,s){var i,a,l,c=8*s-o-1,u=(1<>1,h=23===o?Math.pow(2,-24)-Math.pow(2,-77):0,f=r?0:s-1,d=r?1:-1,m=t<0||0===t&&1/t<0?1:0;for(t=Math.abs(t),isNaN(t)||t===1/0?(a=isNaN(t)?1:0,i=u):(i=Math.floor(Math.log(t)/Math.LN2),t*(l=Math.pow(2,-i))<1&&(i--,l*=2),(t+=i+p>=1?h/l:h*Math.pow(2,1-p))*l>=2&&(i++,l/=2),i+p>=u?(a=0,i=u):i+p>=1?(a=(t*l-1)*Math.pow(2,o),i+=p):(a=t*Math.pow(2,p-1)*Math.pow(2,o),i=0));o>=8;e[n+f]=255&a,f+=d,a/=256,o-=8);for(i=i<0;e[n+f]=255&i,f+=d,i/=256,c-=8);e[n+f-d]|=128*m}},43393:function(e){e.exports=function(){"use strict";var e=Array.prototype.slice;function t(e,t){t&&(e.prototype=Object.create(t.prototype)),e.prototype.constructor=e}function n(e){return i(e)?e:K(e)}function r(e){return a(e)?e:H(e)}function o(e){return l(e)?e:G(e)}function s(e){return i(e)&&!c(e)?e:Z(e)}function i(e){return!(!e||!e[p])}function a(e){return!(!e||!e[h])}function l(e){return!(!e||!e[f])}function c(e){return a(e)||l(e)}function u(e){return!(!e||!e[d])}t(r,n),t(o,n),t(s,n),n.isIterable=i,n.isKeyed=a,n.isIndexed=l,n.isAssociative=c,n.isOrdered=u,n.Keyed=r,n.Indexed=o,n.Set=s;var p="@@__IMMUTABLE_ITERABLE__@@",h="@@__IMMUTABLE_KEYED__@@",f="@@__IMMUTABLE_INDEXED__@@",d="@@__IMMUTABLE_ORDERED__@@",m="delete",g=5,y=1<>>0;if(""+n!==t||4294967295===n)return NaN;t=n}return t<0?O(e)+t:t}function A(){return!0}function C(e,t,n){return(0===e||void 0!==n&&e<=-n)&&(void 0===t||void 0!==n&&t>=n)}function P(e,t){return I(e,t,0)}function N(e,t){return I(e,t,t)}function I(e,t,n){return void 0===e?n:e<0?Math.max(0,t+e):void 0===t?e:Math.min(t,e)}var T=0,R=1,M=2,D="function"==typeof Symbol&&Symbol.iterator,F="@@iterator",L=D||F;function B(e){this.next=e}function $(e,t,n,r){var o=0===e?t:1===e?n:[t,n];return r?r.value=o:r={value:o,done:!1},r}function q(){return{value:void 0,done:!0}}function U(e){return!!W(e)}function z(e){return e&&"function"==typeof e.next}function V(e){var t=W(e);return t&&t.call(e)}function W(e){var t=e&&(D&&e[D]||e[F]);if("function"==typeof t)return t}function J(e){return e&&"number"==typeof e.length}function K(e){return null==e?ie():i(e)?e.toSeq():ce(e)}function H(e){return null==e?ie().toKeyedSeq():i(e)?a(e)?e.toSeq():e.fromEntrySeq():ae(e)}function G(e){return null==e?ie():i(e)?a(e)?e.entrySeq():e.toIndexedSeq():le(e)}function Z(e){return(null==e?ie():i(e)?a(e)?e.entrySeq():e:le(e)).toSetSeq()}B.prototype.toString=function(){return"[Iterator]"},B.KEYS=T,B.VALUES=R,B.ENTRIES=M,B.prototype.inspect=B.prototype.toSource=function(){return this.toString()},B.prototype[L]=function(){return this},t(K,n),K.of=function(){return K(arguments)},K.prototype.toSeq=function(){return this},K.prototype.toString=function(){return this.__toString("Seq {","}")},K.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},K.prototype.__iterate=function(e,t){return pe(this,e,t,!0)},K.prototype.__iterator=function(e,t){return he(this,e,t,!0)},t(H,K),H.prototype.toKeyedSeq=function(){return this},t(G,K),G.of=function(){return G(arguments)},G.prototype.toIndexedSeq=function(){return this},G.prototype.toString=function(){return this.__toString("Seq [","]")},G.prototype.__iterate=function(e,t){return pe(this,e,t,!1)},G.prototype.__iterator=function(e,t){return he(this,e,t,!1)},t(Z,K),Z.of=function(){return Z(arguments)},Z.prototype.toSetSeq=function(){return this},K.isSeq=se,K.Keyed=H,K.Set=Z,K.Indexed=G;var Y,X,Q,ee="@@__IMMUTABLE_SEQ__@@";function te(e){this._array=e,this.size=e.length}function ne(e){var t=Object.keys(e);this._object=e,this._keys=t,this.size=t.length}function re(e){this._iterable=e,this.size=e.length||e.size}function oe(e){this._iterator=e,this._iteratorCache=[]}function se(e){return!(!e||!e[ee])}function ie(){return Y||(Y=new te([]))}function ae(e){var t=Array.isArray(e)?new te(e).fromEntrySeq():z(e)?new oe(e).fromEntrySeq():U(e)?new re(e).fromEntrySeq():"object"==typeof e?new ne(e):void 0;if(!t)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+e);return t}function le(e){var t=ue(e);if(!t)throw new TypeError("Expected Array or iterable object of values: "+e);return t}function ce(e){var t=ue(e)||"object"==typeof e&&new ne(e);if(!t)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+e);return t}function ue(e){return J(e)?new te(e):z(e)?new oe(e):U(e)?new re(e):void 0}function pe(e,t,n,r){var o=e._cache;if(o){for(var s=o.length-1,i=0;i<=s;i++){var a=o[n?s-i:i];if(!1===t(a[1],r?a[0]:i,e))return i+1}return i}return e.__iterateUncached(t,n)}function he(e,t,n,r){var o=e._cache;if(o){var s=o.length-1,i=0;return new B((function(){var e=o[n?s-i:i];return i++>s?q():$(t,r?e[0]:i-1,e[1])}))}return e.__iteratorUncached(t,n)}function fe(e,t){return t?de(t,e,"",{"":e}):me(e)}function de(e,t,n,r){return Array.isArray(t)?e.call(r,n,G(t).map((function(n,r){return de(e,n,r,t)}))):ge(t)?e.call(r,n,H(t).map((function(n,r){return de(e,n,r,t)}))):t}function me(e){return Array.isArray(e)?G(e).map(me).toList():ge(e)?H(e).map(me).toMap():e}function ge(e){return e&&(e.constructor===Object||void 0===e.constructor)}function ye(e,t){if(e===t||e!=e&&t!=t)return!0;if(!e||!t)return!1;if("function"==typeof e.valueOf&&"function"==typeof t.valueOf){if((e=e.valueOf())===(t=t.valueOf())||e!=e&&t!=t)return!0;if(!e||!t)return!1}return!("function"!=typeof e.equals||"function"!=typeof t.equals||!e.equals(t))}function ve(e,t){if(e===t)return!0;if(!i(t)||void 0!==e.size&&void 0!==t.size&&e.size!==t.size||void 0!==e.__hash&&void 0!==t.__hash&&e.__hash!==t.__hash||a(e)!==a(t)||l(e)!==l(t)||u(e)!==u(t))return!1;if(0===e.size&&0===t.size)return!0;var n=!c(e);if(u(e)){var r=e.entries();return t.every((function(e,t){var o=r.next().value;return o&&ye(o[1],e)&&(n||ye(o[0],t))}))&&r.next().done}var o=!1;if(void 0===e.size)if(void 0===t.size)"function"==typeof e.cacheResult&&e.cacheResult();else{o=!0;var s=e;e=t,t=s}var p=!0,h=t.__iterate((function(t,r){if(n?!e.has(t):o?!ye(t,e.get(r,b)):!ye(e.get(r,b),t))return p=!1,!1}));return p&&e.size===h}function be(e,t){if(!(this instanceof be))return new be(e,t);if(this._value=e,this.size=void 0===t?1/0:Math.max(0,t),0===this.size){if(X)return X;X=this}}function we(e,t){if(!e)throw new Error(t)}function Ee(e,t,n){if(!(this instanceof Ee))return new Ee(e,t,n);if(we(0!==n,"Cannot step a Range by 0"),e=e||0,void 0===t&&(t=1/0),n=void 0===n?1:Math.abs(n),tr?q():$(e,o,n[t?r-o++:o++])}))},t(ne,H),ne.prototype.get=function(e,t){return void 0===t||this.has(e)?this._object[e]:t},ne.prototype.has=function(e){return this._object.hasOwnProperty(e)},ne.prototype.__iterate=function(e,t){for(var n=this._object,r=this._keys,o=r.length-1,s=0;s<=o;s++){var i=r[t?o-s:s];if(!1===e(n[i],i,this))return s+1}return s},ne.prototype.__iterator=function(e,t){var n=this._object,r=this._keys,o=r.length-1,s=0;return new B((function(){var i=r[t?o-s:s];return s++>o?q():$(e,i,n[i])}))},ne.prototype[d]=!0,t(re,G),re.prototype.__iterateUncached=function(e,t){if(t)return this.cacheResult().__iterate(e,t);var n=V(this._iterable),r=0;if(z(n))for(var o;!(o=n.next()).done&&!1!==e(o.value,r++,this););return r},re.prototype.__iteratorUncached=function(e,t){if(t)return this.cacheResult().__iterator(e,t);var n=V(this._iterable);if(!z(n))return new B(q);var r=0;return new B((function(){var t=n.next();return t.done?t:$(e,r++,t.value)}))},t(oe,G),oe.prototype.__iterateUncached=function(e,t){if(t)return this.cacheResult().__iterate(e,t);for(var n,r=this._iterator,o=this._iteratorCache,s=0;s=r.length){var t=n.next();if(t.done)return t;r[o]=t.value}return $(e,o,r[o++])}))},t(be,G),be.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},be.prototype.get=function(e,t){return this.has(e)?this._value:t},be.prototype.includes=function(e){return ye(this._value,e)},be.prototype.slice=function(e,t){var n=this.size;return C(e,t,n)?this:new be(this._value,N(t,n)-P(e,n))},be.prototype.reverse=function(){return this},be.prototype.indexOf=function(e){return ye(this._value,e)?0:-1},be.prototype.lastIndexOf=function(e){return ye(this._value,e)?this.size:-1},be.prototype.__iterate=function(e,t){for(var n=0;n=0&&t=0&&nn?q():$(e,s++,i)}))},Ee.prototype.equals=function(e){return e instanceof Ee?this._start===e._start&&this._end===e._end&&this._step===e._step:ve(this,e)},t(xe,n),t(Se,xe),t(_e,xe),t(je,xe),xe.Keyed=Se,xe.Indexed=_e,xe.Set=je;var Oe="function"==typeof Math.imul&&-2===Math.imul(4294967295,2)?Math.imul:function(e,t){var n=65535&(e|=0),r=65535&(t|=0);return n*r+((e>>>16)*r+n*(t>>>16)<<16>>>0)|0};function ke(e){return e>>>1&1073741824|3221225471&e}function Ae(e){if(!1===e||null==e)return 0;if("function"==typeof e.valueOf&&(!1===(e=e.valueOf())||null==e))return 0;if(!0===e)return 1;var t=typeof e;if("number"===t){if(e!=e||e===1/0)return 0;var n=0|e;for(n!==e&&(n^=4294967295*e);e>4294967295;)n^=e/=4294967295;return ke(n)}if("string"===t)return e.length>Be?Ce(e):Pe(e);if("function"==typeof e.hashCode)return e.hashCode();if("object"===t)return Ne(e);if("function"==typeof e.toString)return Pe(e.toString());throw new Error("Value type "+t+" cannot be hashed.")}function Ce(e){var t=Ue[e];return void 0===t&&(t=Pe(e),qe===$e&&(qe=0,Ue={}),qe++,Ue[e]=t),t}function Pe(e){for(var t=0,n=0;n0)switch(e.nodeType){case 1:return e.uniqueID;case 9:return e.documentElement&&e.documentElement.uniqueID}}var Me,De="function"==typeof WeakMap;De&&(Me=new WeakMap);var Fe=0,Le="__immutablehash__";"function"==typeof Symbol&&(Le=Symbol(Le));var Be=16,$e=255,qe=0,Ue={};function ze(e){we(e!==1/0,"Cannot perform this action with an infinite size.")}function Ve(e){return null==e?ot():We(e)&&!u(e)?e:ot().withMutations((function(t){var n=r(e);ze(n.size),n.forEach((function(e,n){return t.set(n,e)}))}))}function We(e){return!(!e||!e[Ke])}t(Ve,Se),Ve.of=function(){var t=e.call(arguments,0);return ot().withMutations((function(e){for(var n=0;n=t.length)throw new Error("Missing value for key: "+t[n]);e.set(t[n],t[n+1])}}))},Ve.prototype.toString=function(){return this.__toString("Map {","}")},Ve.prototype.get=function(e,t){return this._root?this._root.get(0,void 0,e,t):t},Ve.prototype.set=function(e,t){return st(this,e,t)},Ve.prototype.setIn=function(e,t){return this.updateIn(e,b,(function(){return t}))},Ve.prototype.remove=function(e){return st(this,e,b)},Ve.prototype.deleteIn=function(e){return this.updateIn(e,(function(){return b}))},Ve.prototype.update=function(e,t,n){return 1===arguments.length?e(this):this.updateIn([e],t,n)},Ve.prototype.updateIn=function(e,t,n){n||(n=t,t=void 0);var r=gt(this,xn(e),t,n);return r===b?void 0:r},Ve.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):ot()},Ve.prototype.merge=function(){return ht(this,void 0,arguments)},Ve.prototype.mergeWith=function(t){return ht(this,t,e.call(arguments,1))},Ve.prototype.mergeIn=function(t){var n=e.call(arguments,1);return this.updateIn(t,ot(),(function(e){return"function"==typeof e.merge?e.merge.apply(e,n):n[n.length-1]}))},Ve.prototype.mergeDeep=function(){return ht(this,ft,arguments)},Ve.prototype.mergeDeepWith=function(t){var n=e.call(arguments,1);return ht(this,dt(t),n)},Ve.prototype.mergeDeepIn=function(t){var n=e.call(arguments,1);return this.updateIn(t,ot(),(function(e){return"function"==typeof e.mergeDeep?e.mergeDeep.apply(e,n):n[n.length-1]}))},Ve.prototype.sort=function(e){return Ut(pn(this,e))},Ve.prototype.sortBy=function(e,t){return Ut(pn(this,t,e))},Ve.prototype.withMutations=function(e){var t=this.asMutable();return e(t),t.wasAltered()?t.__ensureOwner(this.__ownerID):this},Ve.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new _)},Ve.prototype.asImmutable=function(){return this.__ensureOwner()},Ve.prototype.wasAltered=function(){return this.__altered},Ve.prototype.__iterator=function(e,t){return new et(this,e,t)},Ve.prototype.__iterate=function(e,t){var n=this,r=0;return this._root&&this._root.iterate((function(t){return r++,e(t[1],t[0],n)}),t),r},Ve.prototype.__ensureOwner=function(e){return e===this.__ownerID?this:e?rt(this.size,this._root,e,this.__hash):(this.__ownerID=e,this.__altered=!1,this)},Ve.isMap=We;var Je,Ke="@@__IMMUTABLE_MAP__@@",He=Ve.prototype;function Ge(e,t){this.ownerID=e,this.entries=t}function Ze(e,t,n){this.ownerID=e,this.bitmap=t,this.nodes=n}function Ye(e,t,n){this.ownerID=e,this.count=t,this.nodes=n}function Xe(e,t,n){this.ownerID=e,this.keyHash=t,this.entries=n}function Qe(e,t,n){this.ownerID=e,this.keyHash=t,this.entry=n}function et(e,t,n){this._type=t,this._reverse=n,this._stack=e._root&&nt(e._root)}function tt(e,t){return $(e,t[0],t[1])}function nt(e,t){return{node:e,index:0,__prev:t}}function rt(e,t,n,r){var o=Object.create(He);return o.size=e,o._root=t,o.__ownerID=n,o.__hash=r,o.__altered=!1,o}function ot(){return Je||(Je=rt(0))}function st(e,t,n){var r,o;if(e._root){var s=x(w),i=x(E);if(r=it(e._root,e.__ownerID,0,void 0,t,n,s,i),!i.value)return e;o=e.size+(s.value?n===b?-1:1:0)}else{if(n===b)return e;o=1,r=new Ge(e.__ownerID,[[t,n]])}return e.__ownerID?(e.size=o,e._root=r,e.__hash=void 0,e.__altered=!0,e):r?rt(o,r):ot()}function it(e,t,n,r,o,s,i,a){return e?e.update(t,n,r,o,s,i,a):s===b?e:(S(a),S(i),new Qe(t,r,[o,s]))}function at(e){return e.constructor===Qe||e.constructor===Xe}function lt(e,t,n,r,o){if(e.keyHash===r)return new Xe(t,r,[e.entry,o]);var s,i=(0===n?e.keyHash:e.keyHash>>>n)&v,a=(0===n?r:r>>>n)&v;return new Ze(t,1<>>=1)i[a]=1&n?t[s++]:void 0;return i[r]=o,new Ye(e,s+1,i)}function ht(e,t,n){for(var o=[],s=0;s>1&1431655765))+(e>>2&858993459))+(e>>4)&252645135,e+=e>>8,127&(e+=e>>16)}function vt(e,t,n,r){var o=r?e:j(e);return o[t]=n,o}function bt(e,t,n,r){var o=e.length+1;if(r&&t+1===o)return e[t]=n,e;for(var s=new Array(o),i=0,a=0;a=Et)return ct(e,l,r,o);var h=e&&e===this.ownerID,f=h?l:j(l);return p?a?c===u-1?f.pop():f[c]=f.pop():f[c]=[r,o]:f.push([r,o]),h?(this.entries=f,this):new Ge(e,f)}},Ze.prototype.get=function(e,t,n,r){void 0===t&&(t=Ae(n));var o=1<<((0===e?t:t>>>e)&v),s=this.bitmap;return 0==(s&o)?r:this.nodes[yt(s&o-1)].get(e+g,t,n,r)},Ze.prototype.update=function(e,t,n,r,o,s,i){void 0===n&&(n=Ae(r));var a=(0===t?n:n>>>t)&v,l=1<=xt)return pt(e,h,c,a,d);if(u&&!d&&2===h.length&&at(h[1^p]))return h[1^p];if(u&&d&&1===h.length&&at(d))return d;var m=e&&e===this.ownerID,y=u?d?c:c^l:c|l,w=u?d?vt(h,p,d,m):wt(h,p,m):bt(h,p,d,m);return m?(this.bitmap=y,this.nodes=w,this):new Ze(e,y,w)},Ye.prototype.get=function(e,t,n,r){void 0===t&&(t=Ae(n));var o=(0===e?t:t>>>e)&v,s=this.nodes[o];return s?s.get(e+g,t,n,r):r},Ye.prototype.update=function(e,t,n,r,o,s,i){void 0===n&&(n=Ae(r));var a=(0===t?n:n>>>t)&v,l=o===b,c=this.nodes,u=c[a];if(l&&!u)return this;var p=it(u,e,t+g,n,r,o,s,i);if(p===u)return this;var h=this.count;if(u){if(!p&&--h0&&r=0&&e>>t&v;if(r>=this.array.length)return new At([],e);var o,s=0===r;if(t>0){var i=this.array[r];if((o=i&&i.removeBefore(e,t-g,n))===i&&s)return this}if(s&&!o)return this;var a=Ft(this,e);if(!s)for(var l=0;l>>t&v;if(o>=this.array.length)return this;if(t>0){var s=this.array[o];if((r=s&&s.removeAfter(e,t-g,n))===s&&o===this.array.length-1)return this}var i=Ft(this,e);return i.array.splice(o+1),r&&(i.array[o]=r),i};var Ct,Pt,Nt={};function It(e,t){var n=e._origin,r=e._capacity,o=qt(r),s=e._tail;return i(e._root,e._level,0);function i(e,t,n){return 0===t?a(e,n):l(e,t,n)}function a(e,i){var a=i===o?s&&s.array:e&&e.array,l=i>n?0:n-i,c=r-i;return c>y&&(c=y),function(){if(l===c)return Nt;var e=t?--c:l++;return a&&a[e]}}function l(e,o,s){var a,l=e&&e.array,c=s>n?0:n-s>>o,u=1+(r-s>>o);return u>y&&(u=y),function(){for(;;){if(a){var e=a();if(e!==Nt)return e;a=null}if(c===u)return Nt;var n=t?--u:c++;a=i(l&&l[n],o-g,s+(n<=e.size||t<0)return e.withMutations((function(e){t<0?Bt(e,t).set(0,n):Bt(e,0,t+1).set(t,n)}));t+=e._origin;var r=e._tail,o=e._root,s=x(E);return t>=qt(e._capacity)?r=Dt(r,e.__ownerID,0,t,n,s):o=Dt(o,e.__ownerID,e._level,t,n,s),s.value?e.__ownerID?(e._root=o,e._tail=r,e.__hash=void 0,e.__altered=!0,e):Tt(e._origin,e._capacity,e._level,o,r):e}function Dt(e,t,n,r,o,s){var i,a=r>>>n&v,l=e&&a0){var c=e&&e.array[a],u=Dt(c,t,n-g,r,o,s);return u===c?e:((i=Ft(e,t)).array[a]=u,i)}return l&&e.array[a]===o?e:(S(s),i=Ft(e,t),void 0===o&&a===i.array.length-1?i.array.pop():i.array[a]=o,i)}function Ft(e,t){return t&&e&&t===e.ownerID?e:new At(e?e.array.slice():[],t)}function Lt(e,t){if(t>=qt(e._capacity))return e._tail;if(t<1<0;)n=n.array[t>>>r&v],r-=g;return n}}function Bt(e,t,n){void 0!==t&&(t|=0),void 0!==n&&(n|=0);var r=e.__ownerID||new _,o=e._origin,s=e._capacity,i=o+t,a=void 0===n?s:n<0?s+n:o+n;if(i===o&&a===s)return e;if(i>=a)return e.clear();for(var l=e._level,c=e._root,u=0;i+u<0;)c=new At(c&&c.array.length?[void 0,c]:[],r),u+=1<<(l+=g);u&&(i+=u,o+=u,a+=u,s+=u);for(var p=qt(s),h=qt(a);h>=1<p?new At([],r):f;if(f&&h>p&&ig;y-=g){var b=p>>>y&v;m=m.array[b]=Ft(m.array[b],r)}m.array[p>>>g&v]=f}if(a=h)i-=h,a-=h,l=g,c=null,d=d&&d.removeBefore(r,0,i);else if(i>o||h>>l&v;if(w!==h>>>l&v)break;w&&(u+=(1<o&&(c=c.removeBefore(r,l,i-u)),c&&hs&&(s=c.size),i(l)||(c=c.map((function(e){return fe(e)}))),r.push(c)}return s>e.size&&(e=e.setSize(s)),mt(e,t,r)}function qt(e){return e>>g<=y&&i.size>=2*s.size?(r=(o=i.filter((function(e,t){return void 0!==e&&a!==t}))).toKeyedSeq().map((function(e){return e[0]})).flip().toMap(),e.__ownerID&&(r.__ownerID=o.__ownerID=e.__ownerID)):(r=s.remove(t),o=a===i.size-1?i.pop():i.set(a,void 0))}else if(l){if(n===i.get(a)[1])return e;r=s,o=i.set(a,[t,n])}else r=s.set(t,i.size),o=i.set(i.size,[t,n]);return e.__ownerID?(e.size=r.size,e._map=r,e._list=o,e.__hash=void 0,e):Vt(r,o)}function Kt(e,t){this._iter=e,this._useKeys=t,this.size=e.size}function Ht(e){this._iter=e,this.size=e.size}function Gt(e){this._iter=e,this.size=e.size}function Zt(e){this._iter=e,this.size=e.size}function Yt(e){var t=bn(e);return t._iter=e,t.size=e.size,t.flip=function(){return e},t.reverse=function(){var t=e.reverse.apply(this);return t.flip=function(){return e.reverse()},t},t.has=function(t){return e.includes(t)},t.includes=function(t){return e.has(t)},t.cacheResult=wn,t.__iterateUncached=function(t,n){var r=this;return e.__iterate((function(e,n){return!1!==t(n,e,r)}),n)},t.__iteratorUncached=function(t,n){if(t===M){var r=e.__iterator(t,n);return new B((function(){var e=r.next();if(!e.done){var t=e.value[0];e.value[0]=e.value[1],e.value[1]=t}return e}))}return e.__iterator(t===R?T:R,n)},t}function Xt(e,t,n){var r=bn(e);return r.size=e.size,r.has=function(t){return e.has(t)},r.get=function(r,o){var s=e.get(r,b);return s===b?o:t.call(n,s,r,e)},r.__iterateUncached=function(r,o){var s=this;return e.__iterate((function(e,o,i){return!1!==r(t.call(n,e,o,i),o,s)}),o)},r.__iteratorUncached=function(r,o){var s=e.__iterator(M,o);return new B((function(){var o=s.next();if(o.done)return o;var i=o.value,a=i[0];return $(r,a,t.call(n,i[1],a,e),o)}))},r}function Qt(e,t){var n=bn(e);return n._iter=e,n.size=e.size,n.reverse=function(){return e},e.flip&&(n.flip=function(){var t=Yt(e);return t.reverse=function(){return e.flip()},t}),n.get=function(n,r){return e.get(t?n:-1-n,r)},n.has=function(n){return e.has(t?n:-1-n)},n.includes=function(t){return e.includes(t)},n.cacheResult=wn,n.__iterate=function(t,n){var r=this;return e.__iterate((function(e,n){return t(e,n,r)}),!n)},n.__iterator=function(t,n){return e.__iterator(t,!n)},n}function en(e,t,n,r){var o=bn(e);return r&&(o.has=function(r){var o=e.get(r,b);return o!==b&&!!t.call(n,o,r,e)},o.get=function(r,o){var s=e.get(r,b);return s!==b&&t.call(n,s,r,e)?s:o}),o.__iterateUncached=function(o,s){var i=this,a=0;return e.__iterate((function(e,s,l){if(t.call(n,e,s,l))return a++,o(e,r?s:a-1,i)}),s),a},o.__iteratorUncached=function(o,s){var i=e.__iterator(M,s),a=0;return new B((function(){for(;;){var s=i.next();if(s.done)return s;var l=s.value,c=l[0],u=l[1];if(t.call(n,u,c,e))return $(o,r?c:a++,u,s)}}))},o}function tn(e,t,n){var r=Ve().asMutable();return e.__iterate((function(o,s){r.update(t.call(n,o,s,e),0,(function(e){return e+1}))})),r.asImmutable()}function nn(e,t,n){var r=a(e),o=(u(e)?Ut():Ve()).asMutable();e.__iterate((function(s,i){o.update(t.call(n,s,i,e),(function(e){return(e=e||[]).push(r?[i,s]:s),e}))}));var s=vn(e);return o.map((function(t){return mn(e,s(t))}))}function rn(e,t,n,r){var o=e.size;if(void 0!==t&&(t|=0),void 0!==n&&(n===1/0?n=o:n|=0),C(t,n,o))return e;var s=P(t,o),i=N(n,o);if(s!=s||i!=i)return rn(e.toSeq().cacheResult(),t,n,r);var a,l=i-s;l==l&&(a=l<0?0:l);var c=bn(e);return c.size=0===a?a:e.size&&a||void 0,!r&&se(e)&&a>=0&&(c.get=function(t,n){return(t=k(this,t))>=0&&ta)return q();var e=o.next();return r||t===R?e:$(t,l-1,t===T?void 0:e.value[1],e)}))},c}function on(e,t,n){var r=bn(e);return r.__iterateUncached=function(r,o){var s=this;if(o)return this.cacheResult().__iterate(r,o);var i=0;return e.__iterate((function(e,o,a){return t.call(n,e,o,a)&&++i&&r(e,o,s)})),i},r.__iteratorUncached=function(r,o){var s=this;if(o)return this.cacheResult().__iterator(r,o);var i=e.__iterator(M,o),a=!0;return new B((function(){if(!a)return q();var e=i.next();if(e.done)return e;var o=e.value,l=o[0],c=o[1];return t.call(n,c,l,s)?r===M?e:$(r,l,c,e):(a=!1,q())}))},r}function sn(e,t,n,r){var o=bn(e);return o.__iterateUncached=function(o,s){var i=this;if(s)return this.cacheResult().__iterate(o,s);var a=!0,l=0;return e.__iterate((function(e,s,c){if(!a||!(a=t.call(n,e,s,c)))return l++,o(e,r?s:l-1,i)})),l},o.__iteratorUncached=function(o,s){var i=this;if(s)return this.cacheResult().__iterator(o,s);var a=e.__iterator(M,s),l=!0,c=0;return new B((function(){var e,s,u;do{if((e=a.next()).done)return r||o===R?e:$(o,c++,o===T?void 0:e.value[1],e);var p=e.value;s=p[0],u=p[1],l&&(l=t.call(n,u,s,i))}while(l);return o===M?e:$(o,s,u,e)}))},o}function an(e,t){var n=a(e),o=[e].concat(t).map((function(e){return i(e)?n&&(e=r(e)):e=n?ae(e):le(Array.isArray(e)?e:[e]),e})).filter((function(e){return 0!==e.size}));if(0===o.length)return e;if(1===o.length){var s=o[0];if(s===e||n&&a(s)||l(e)&&l(s))return s}var c=new te(o);return n?c=c.toKeyedSeq():l(e)||(c=c.toSetSeq()),(c=c.flatten(!0)).size=o.reduce((function(e,t){if(void 0!==e){var n=t.size;if(void 0!==n)return e+n}}),0),c}function ln(e,t,n){var r=bn(e);return r.__iterateUncached=function(r,o){var s=0,a=!1;function l(e,c){var u=this;e.__iterate((function(e,o){return(!t||c0}function dn(e,t,r){var o=bn(e);return o.size=new te(r).map((function(e){return e.size})).min(),o.__iterate=function(e,t){for(var n,r=this.__iterator(R,t),o=0;!(n=r.next()).done&&!1!==e(n.value,o++,this););return o},o.__iteratorUncached=function(e,o){var s=r.map((function(e){return e=n(e),V(o?e.reverse():e)})),i=0,a=!1;return new B((function(){var n;return a||(n=s.map((function(e){return e.next()})),a=n.some((function(e){return e.done}))),a?q():$(e,i++,t.apply(null,n.map((function(e){return e.value}))))}))},o}function mn(e,t){return se(e)?t:e.constructor(t)}function gn(e){if(e!==Object(e))throw new TypeError("Expected [K, V] tuple: "+e)}function yn(e){return ze(e.size),O(e)}function vn(e){return a(e)?r:l(e)?o:s}function bn(e){return Object.create((a(e)?H:l(e)?G:Z).prototype)}function wn(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):K.prototype.cacheResult.call(this)}function En(e,t){return e>t?1:e=0;n--)t={value:arguments[n],next:t};return this.__ownerID?(this.size=e,this._head=t,this.__hash=void 0,this.__altered=!0,this):Hn(e,t)},zn.prototype.pushAll=function(e){if(0===(e=o(e)).size)return this;ze(e.size);var t=this.size,n=this._head;return e.reverse().forEach((function(e){t++,n={value:e,next:n}})),this.__ownerID?(this.size=t,this._head=n,this.__hash=void 0,this.__altered=!0,this):Hn(t,n)},zn.prototype.pop=function(){return this.slice(1)},zn.prototype.unshift=function(){return this.push.apply(this,arguments)},zn.prototype.unshiftAll=function(e){return this.pushAll(e)},zn.prototype.shift=function(){return this.pop.apply(this,arguments)},zn.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):Gn()},zn.prototype.slice=function(e,t){if(C(e,t,this.size))return this;var n=P(e,this.size);if(N(t,this.size)!==this.size)return _e.prototype.slice.call(this,e,t);for(var r=this.size-n,o=this._head;n--;)o=o.next;return this.__ownerID?(this.size=r,this._head=o,this.__hash=void 0,this.__altered=!0,this):Hn(r,o)},zn.prototype.__ensureOwner=function(e){return e===this.__ownerID?this:e?Hn(this.size,this._head,e,this.__hash):(this.__ownerID=e,this.__altered=!1,this)},zn.prototype.__iterate=function(e,t){if(t)return this.reverse().__iterate(e);for(var n=0,r=this._head;r&&!1!==e(r.value,n++,this);)r=r.next;return n},zn.prototype.__iterator=function(e,t){if(t)return this.reverse().__iterator(e);var n=0,r=this._head;return new B((function(){if(r){var t=r.value;return r=r.next,$(e,n++,t)}return q()}))},zn.isStack=Vn;var Wn,Jn="@@__IMMUTABLE_STACK__@@",Kn=zn.prototype;function Hn(e,t,n,r){var o=Object.create(Kn);return o.size=e,o._head=t,o.__ownerID=n,o.__hash=r,o.__altered=!1,o}function Gn(){return Wn||(Wn=Hn(0))}function Zn(e,t){var n=function(n){e.prototype[n]=t[n]};return Object.keys(t).forEach(n),Object.getOwnPropertySymbols&&Object.getOwnPropertySymbols(t).forEach(n),e}Kn[Jn]=!0,Kn.withMutations=He.withMutations,Kn.asMutable=He.asMutable,Kn.asImmutable=He.asImmutable,Kn.wasAltered=He.wasAltered,n.Iterator=B,Zn(n,{toArray:function(){ze(this.size);var e=new Array(this.size||0);return this.valueSeq().__iterate((function(t,n){e[n]=t})),e},toIndexedSeq:function(){return new Ht(this)},toJS:function(){return this.toSeq().map((function(e){return e&&"function"==typeof e.toJS?e.toJS():e})).__toJS()},toJSON:function(){return this.toSeq().map((function(e){return e&&"function"==typeof e.toJSON?e.toJSON():e})).__toJS()},toKeyedSeq:function(){return new Kt(this,!0)},toMap:function(){return Ve(this.toKeyedSeq())},toObject:function(){ze(this.size);var e={};return this.__iterate((function(t,n){e[n]=t})),e},toOrderedMap:function(){return Ut(this.toKeyedSeq())},toOrderedSet:function(){return Fn(a(this)?this.valueSeq():this)},toSet:function(){return Cn(a(this)?this.valueSeq():this)},toSetSeq:function(){return new Gt(this)},toSeq:function(){return l(this)?this.toIndexedSeq():a(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return zn(a(this)?this.valueSeq():this)},toList:function(){return _t(a(this)?this.valueSeq():this)},toString:function(){return"[Iterable]"},__toString:function(e,t){return 0===this.size?e+t:e+" "+this.toSeq().map(this.__toStringMapper).join(", ")+" "+t},concat:function(){return mn(this,an(this,e.call(arguments,0)))},includes:function(e){return this.some((function(t){return ye(t,e)}))},entries:function(){return this.__iterator(M)},every:function(e,t){ze(this.size);var n=!0;return this.__iterate((function(r,o,s){if(!e.call(t,r,o,s))return n=!1,!1})),n},filter:function(e,t){return mn(this,en(this,e,t,!0))},find:function(e,t,n){var r=this.findEntry(e,t);return r?r[1]:n},forEach:function(e,t){return ze(this.size),this.__iterate(t?e.bind(t):e)},join:function(e){ze(this.size),e=void 0!==e?""+e:",";var t="",n=!0;return this.__iterate((function(r){n?n=!1:t+=e,t+=null!=r?r.toString():""})),t},keys:function(){return this.__iterator(T)},map:function(e,t){return mn(this,Xt(this,e,t))},reduce:function(e,t,n){var r,o;return ze(this.size),arguments.length<2?o=!0:r=t,this.__iterate((function(t,s,i){o?(o=!1,r=t):r=e.call(n,r,t,s,i)})),r},reduceRight:function(e,t,n){var r=this.toKeyedSeq().reverse();return r.reduce.apply(r,arguments)},reverse:function(){return mn(this,Qt(this,!0))},slice:function(e,t){return mn(this,rn(this,e,t,!0))},some:function(e,t){return!this.every(tr(e),t)},sort:function(e){return mn(this,pn(this,e))},values:function(){return this.__iterator(R)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some((function(){return!0}))},count:function(e,t){return O(e?this.toSeq().filter(e,t):this)},countBy:function(e,t){return tn(this,e,t)},equals:function(e){return ve(this,e)},entrySeq:function(){var e=this;if(e._cache)return new te(e._cache);var t=e.toSeq().map(er).toIndexedSeq();return t.fromEntrySeq=function(){return e.toSeq()},t},filterNot:function(e,t){return this.filter(tr(e),t)},findEntry:function(e,t,n){var r=n;return this.__iterate((function(n,o,s){if(e.call(t,n,o,s))return r=[o,n],!1})),r},findKey:function(e,t){var n=this.findEntry(e,t);return n&&n[0]},findLast:function(e,t,n){return this.toKeyedSeq().reverse().find(e,t,n)},findLastEntry:function(e,t,n){return this.toKeyedSeq().reverse().findEntry(e,t,n)},findLastKey:function(e,t){return this.toKeyedSeq().reverse().findKey(e,t)},first:function(){return this.find(A)},flatMap:function(e,t){return mn(this,cn(this,e,t))},flatten:function(e){return mn(this,ln(this,e,!0))},fromEntrySeq:function(){return new Zt(this)},get:function(e,t){return this.find((function(t,n){return ye(n,e)}),void 0,t)},getIn:function(e,t){for(var n,r=this,o=xn(e);!(n=o.next()).done;){var s=n.value;if((r=r&&r.get?r.get(s,b):b)===b)return t}return r},groupBy:function(e,t){return nn(this,e,t)},has:function(e){return this.get(e,b)!==b},hasIn:function(e){return this.getIn(e,b)!==b},isSubset:function(e){return e="function"==typeof e.includes?e:n(e),this.every((function(t){return e.includes(t)}))},isSuperset:function(e){return(e="function"==typeof e.isSubset?e:n(e)).isSubset(this)},keyOf:function(e){return this.findKey((function(t){return ye(t,e)}))},keySeq:function(){return this.toSeq().map(Qn).toIndexedSeq()},last:function(){return this.toSeq().reverse().first()},lastKeyOf:function(e){return this.toKeyedSeq().reverse().keyOf(e)},max:function(e){return hn(this,e)},maxBy:function(e,t){return hn(this,t,e)},min:function(e){return hn(this,e?nr(e):sr)},minBy:function(e,t){return hn(this,t?nr(t):sr,e)},rest:function(){return this.slice(1)},skip:function(e){return this.slice(Math.max(0,e))},skipLast:function(e){return mn(this,this.toSeq().reverse().skip(e).reverse())},skipWhile:function(e,t){return mn(this,sn(this,e,t,!0))},skipUntil:function(e,t){return this.skipWhile(tr(e),t)},sortBy:function(e,t){return mn(this,pn(this,t,e))},take:function(e){return this.slice(0,Math.max(0,e))},takeLast:function(e){return mn(this,this.toSeq().reverse().take(e).reverse())},takeWhile:function(e,t){return mn(this,on(this,e,t))},takeUntil:function(e,t){return this.takeWhile(tr(e),t)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=ir(this))}});var Yn=n.prototype;Yn[p]=!0,Yn[L]=Yn.values,Yn.__toJS=Yn.toArray,Yn.__toStringMapper=rr,Yn.inspect=Yn.toSource=function(){return this.toString()},Yn.chain=Yn.flatMap,Yn.contains=Yn.includes,Zn(r,{flip:function(){return mn(this,Yt(this))},mapEntries:function(e,t){var n=this,r=0;return mn(this,this.toSeq().map((function(o,s){return e.call(t,[s,o],r++,n)})).fromEntrySeq())},mapKeys:function(e,t){var n=this;return mn(this,this.toSeq().flip().map((function(r,o){return e.call(t,r,o,n)})).flip())}});var Xn=r.prototype;function Qn(e,t){return t}function er(e,t){return[t,e]}function tr(e){return function(){return!e.apply(this,arguments)}}function nr(e){return function(){return-e.apply(this,arguments)}}function rr(e){return"string"==typeof e?JSON.stringify(e):String(e)}function or(){return j(arguments)}function sr(e,t){return et?-1:0}function ir(e){if(e.size===1/0)return 0;var t=u(e),n=a(e),r=t?1:0;return ar(e.__iterate(n?t?function(e,t){r=31*r+lr(Ae(e),Ae(t))|0}:function(e,t){r=r+lr(Ae(e),Ae(t))|0}:t?function(e){r=31*r+Ae(e)|0}:function(e){r=r+Ae(e)|0}),r)}function ar(e,t){return t=Oe(t,3432918353),t=Oe(t<<15|t>>>-15,461845907),t=Oe(t<<13|t>>>-13,5),t=Oe((t=(t+3864292196|0)^e)^t>>>16,2246822507),t=ke((t=Oe(t^t>>>13,3266489909))^t>>>16)}function lr(e,t){return e^t+2654435769+(e<<6)+(e>>2)|0}return Xn[h]=!0,Xn[L]=Yn.entries,Xn.__toJS=Yn.toObject,Xn.__toStringMapper=function(e,t){return JSON.stringify(t)+": "+rr(e)},Zn(o,{toKeyedSeq:function(){return new Kt(this,!1)},filter:function(e,t){return mn(this,en(this,e,t,!1))},findIndex:function(e,t){var n=this.findEntry(e,t);return n?n[0]:-1},indexOf:function(e){var t=this.keyOf(e);return void 0===t?-1:t},lastIndexOf:function(e){var t=this.lastKeyOf(e);return void 0===t?-1:t},reverse:function(){return mn(this,Qt(this,!1))},slice:function(e,t){return mn(this,rn(this,e,t,!1))},splice:function(e,t){var n=arguments.length;if(t=Math.max(0|t,0),0===n||2===n&&!t)return this;e=P(e,e<0?this.count():this.size);var r=this.slice(0,e);return mn(this,1===n?r:r.concat(j(arguments,2),this.slice(e+t)))},findLastIndex:function(e,t){var n=this.findLastEntry(e,t);return n?n[0]:-1},first:function(){return this.get(0)},flatten:function(e){return mn(this,ln(this,e,!1))},get:function(e,t){return(e=k(this,e))<0||this.size===1/0||void 0!==this.size&&e>this.size?t:this.find((function(t,n){return n===e}),void 0,t)},has:function(e){return(e=k(this,e))>=0&&(void 0!==this.size?this.size===1/0||e{"function"==typeof Object.create?e.exports=function(e,t){t&&(e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}))}:e.exports=function(e,t){if(t){e.super_=t;var n=function(){};n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e}}},35823:e=>{e.exports=function(e,t,n,r){var o=new Blob(void 0!==r?[r,e]:[e],{type:n||"application/octet-stream"});if(void 0!==window.navigator.msSaveBlob)window.navigator.msSaveBlob(o,t);else{var s=window.URL&&window.URL.createObjectURL?window.URL.createObjectURL(o):window.webkitURL.createObjectURL(o),i=document.createElement("a");i.style.display="none",i.href=s,i.setAttribute("download",t),void 0===i.download&&i.setAttribute("target","_blank"),document.body.appendChild(i),i.click(),setTimeout((function(){document.body.removeChild(i),window.URL.revokeObjectURL(s)}),200)}}},91296:(e,t,n)=>{var r=NaN,o="[object Symbol]",s=/^\s+|\s+$/g,i=/^[-+]0x[0-9a-f]+$/i,a=/^0b[01]+$/i,l=/^0o[0-7]+$/i,c=parseInt,u="object"==typeof n.g&&n.g&&n.g.Object===Object&&n.g,p="object"==typeof self&&self&&self.Object===Object&&self,h=u||p||Function("return this")(),f=Object.prototype.toString,d=Math.max,m=Math.min,g=function(){return h.Date.now()};function y(e){var t=typeof e;return!!e&&("object"==t||"function"==t)}function v(e){if("number"==typeof e)return e;if(function(e){return"symbol"==typeof e||function(e){return!!e&&"object"==typeof e}(e)&&f.call(e)==o}(e))return r;if(y(e)){var t="function"==typeof e.valueOf?e.valueOf():e;e=y(t)?t+"":t}if("string"!=typeof e)return 0===e?e:+e;e=e.replace(s,"");var n=a.test(e);return n||l.test(e)?c(e.slice(2),n?2:8):i.test(e)?r:+e}e.exports=function(e,t,n){var r,o,s,i,a,l,c=0,u=!1,p=!1,h=!0;if("function"!=typeof e)throw new TypeError("Expected a function");function f(t){var n=r,s=o;return r=o=void 0,c=t,i=e.apply(s,n)}function b(e){var n=e-l;return void 0===l||n>=t||n<0||p&&e-c>=s}function w(){var e=g();if(b(e))return E(e);a=setTimeout(w,function(e){var n=t-(e-l);return p?m(n,s-(e-c)):n}(e))}function E(e){return a=void 0,h&&r?f(e):(r=o=void 0,i)}function x(){var e=g(),n=b(e);if(r=arguments,o=this,l=e,n){if(void 0===a)return function(e){return c=e,a=setTimeout(w,t),u?f(e):i}(l);if(p)return a=setTimeout(w,t),f(l)}return void 0===a&&(a=setTimeout(w,t)),i}return t=v(t)||0,y(n)&&(u=!!n.leading,s=(p="maxWait"in n)?d(v(n.maxWait)||0,t):s,h="trailing"in n?!!n.trailing:h),x.cancel=function(){void 0!==a&&clearTimeout(a),c=0,r=l=o=a=void 0},x.flush=function(){return void 0===a?i:E(g())},x}},18552:(e,t,n)=>{var r=n(10852)(n(55639),"DataView");e.exports=r},1989:(e,t,n)=>{var r=n(51789),o=n(80401),s=n(57667),i=n(21327),a=n(81866);function l(e){var t=-1,n=null==e?0:e.length;for(this.clear();++t{var r=n(3118),o=n(9435);function s(e){this.__wrapped__=e,this.__actions__=[],this.__dir__=1,this.__filtered__=!1,this.__iteratees__=[],this.__takeCount__=4294967295,this.__views__=[]}s.prototype=r(o.prototype),s.prototype.constructor=s,e.exports=s},38407:(e,t,n)=>{var r=n(27040),o=n(14125),s=n(82117),i=n(67518),a=n(54705);function l(e){var t=-1,n=null==e?0:e.length;for(this.clear();++t{var r=n(3118),o=n(9435);function s(e,t){this.__wrapped__=e,this.__actions__=[],this.__chain__=!!t,this.__index__=0,this.__values__=void 0}s.prototype=r(o.prototype),s.prototype.constructor=s,e.exports=s},57071:(e,t,n)=>{var r=n(10852)(n(55639),"Map");e.exports=r},83369:(e,t,n)=>{var r=n(24785),o=n(11285),s=n(96e3),i=n(49916),a=n(95265);function l(e){var t=-1,n=null==e?0:e.length;for(this.clear();++t{var r=n(10852)(n(55639),"Promise");e.exports=r},58525:(e,t,n)=>{var r=n(10852)(n(55639),"Set");e.exports=r},88668:(e,t,n)=>{var r=n(83369),o=n(90619),s=n(72385);function i(e){var t=-1,n=null==e?0:e.length;for(this.__data__=new r;++t{var r=n(38407),o=n(37465),s=n(63779),i=n(67599),a=n(44758),l=n(34309);function c(e){var t=this.__data__=new r(e);this.size=t.size}c.prototype.clear=o,c.prototype.delete=s,c.prototype.get=i,c.prototype.has=a,c.prototype.set=l,e.exports=c},62705:(e,t,n)=>{var r=n(55639).Symbol;e.exports=r},11149:(e,t,n)=>{var r=n(55639).Uint8Array;e.exports=r},70577:(e,t,n)=>{var r=n(10852)(n(55639),"WeakMap");e.exports=r},96874:e=>{e.exports=function(e,t,n){switch(n.length){case 0:return e.call(t);case 1:return e.call(t,n[0]);case 2:return e.call(t,n[0],n[1]);case 3:return e.call(t,n[0],n[1],n[2])}return e.apply(t,n)}},77412:e=>{e.exports=function(e,t){for(var n=-1,r=null==e?0:e.length;++n{e.exports=function(e,t){for(var n=-1,r=null==e?0:e.length,o=0,s=[];++n{var r=n(42118);e.exports=function(e,t){return!!(null==e?0:e.length)&&r(e,t,0)>-1}},14636:(e,t,n)=>{var r=n(22545),o=n(35694),s=n(1469),i=n(44144),a=n(65776),l=n(36719),c=Object.prototype.hasOwnProperty;e.exports=function(e,t){var n=s(e),u=!n&&o(e),p=!n&&!u&&i(e),h=!n&&!u&&!p&&l(e),f=n||u||p||h,d=f?r(e.length,String):[],m=d.length;for(var g in e)!t&&!c.call(e,g)||f&&("length"==g||p&&("offset"==g||"parent"==g)||h&&("buffer"==g||"byteLength"==g||"byteOffset"==g)||a(g,m))||d.push(g);return d}},29932:e=>{e.exports=function(e,t){for(var n=-1,r=null==e?0:e.length,o=Array(r);++n{e.exports=function(e,t){for(var n=-1,r=t.length,o=e.length;++n{e.exports=function(e,t,n,r){var o=-1,s=null==e?0:e.length;for(r&&s&&(n=e[++o]);++o{e.exports=function(e,t){for(var n=-1,r=null==e?0:e.length;++n{e.exports=function(e){return e.split("")}},49029:e=>{var t=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g;e.exports=function(e){return e.match(t)||[]}},86556:(e,t,n)=>{var r=n(89465),o=n(77813);e.exports=function(e,t,n){(void 0!==n&&!o(e[t],n)||void 0===n&&!(t in e))&&r(e,t,n)}},34865:(e,t,n)=>{var r=n(89465),o=n(77813),s=Object.prototype.hasOwnProperty;e.exports=function(e,t,n){var i=e[t];s.call(e,t)&&o(i,n)&&(void 0!==n||t in e)||r(e,t,n)}},18470:(e,t,n)=>{var r=n(77813);e.exports=function(e,t){for(var n=e.length;n--;)if(r(e[n][0],t))return n;return-1}},44037:(e,t,n)=>{var r=n(98363),o=n(3674);e.exports=function(e,t){return e&&r(t,o(t),e)}},63886:(e,t,n)=>{var r=n(98363),o=n(81704);e.exports=function(e,t){return e&&r(t,o(t),e)}},89465:(e,t,n)=>{var r=n(38777);e.exports=function(e,t,n){"__proto__"==t&&r?r(e,t,{configurable:!0,enumerable:!0,value:n,writable:!0}):e[t]=n}},85990:(e,t,n)=>{var r=n(46384),o=n(77412),s=n(34865),i=n(44037),a=n(63886),l=n(64626),c=n(278),u=n(18805),p=n(1911),h=n(58234),f=n(46904),d=n(98882),m=n(43824),g=n(29148),y=n(38517),v=n(1469),b=n(44144),w=n(56688),E=n(13218),x=n(72928),S=n(3674),_=n(81704),j="[object Arguments]",O="[object Function]",k="[object Object]",A={};A[j]=A["[object Array]"]=A["[object ArrayBuffer]"]=A["[object DataView]"]=A["[object Boolean]"]=A["[object Date]"]=A["[object Float32Array]"]=A["[object Float64Array]"]=A["[object Int8Array]"]=A["[object Int16Array]"]=A["[object Int32Array]"]=A["[object Map]"]=A["[object Number]"]=A[k]=A["[object RegExp]"]=A["[object Set]"]=A["[object String]"]=A["[object Symbol]"]=A["[object Uint8Array]"]=A["[object Uint8ClampedArray]"]=A["[object Uint16Array]"]=A["[object Uint32Array]"]=!0,A["[object Error]"]=A[O]=A["[object WeakMap]"]=!1,e.exports=function e(t,n,C,P,N,I){var T,R=1&n,M=2&n,D=4&n;if(C&&(T=N?C(t,P,N,I):C(t)),void 0!==T)return T;if(!E(t))return t;var F=v(t);if(F){if(T=m(t),!R)return c(t,T)}else{var L=d(t),B=L==O||"[object GeneratorFunction]"==L;if(b(t))return l(t,R);if(L==k||L==j||B&&!N){if(T=M||B?{}:y(t),!R)return M?p(t,a(T,t)):u(t,i(T,t))}else{if(!A[L])return N?t:{};T=g(t,L,R)}}I||(I=new r);var $=I.get(t);if($)return $;I.set(t,T),x(t)?t.forEach((function(r){T.add(e(r,n,C,r,t,I))})):w(t)&&t.forEach((function(r,o){T.set(o,e(r,n,C,o,t,I))}));var q=F?void 0:(D?M?f:h:M?_:S)(t);return o(q||t,(function(r,o){q&&(r=t[o=r]),s(T,o,e(r,n,C,o,t,I))})),T}},3118:(e,t,n)=>{var r=n(13218),o=Object.create,s=function(){function e(){}return function(t){if(!r(t))return{};if(o)return o(t);e.prototype=t;var n=new e;return e.prototype=void 0,n}}();e.exports=s},89881:(e,t,n)=>{var r=n(47816),o=n(99291)(r);e.exports=o},41848:e=>{e.exports=function(e,t,n,r){for(var o=e.length,s=n+(r?1:-1);r?s--:++s{var r=n(62488),o=n(37285);e.exports=function e(t,n,s,i,a){var l=-1,c=t.length;for(s||(s=o),a||(a=[]);++l0&&s(u)?n>1?e(u,n-1,s,i,a):r(a,u):i||(a[a.length]=u)}return a}},28483:(e,t,n)=>{var r=n(25063)();e.exports=r},47816:(e,t,n)=>{var r=n(28483),o=n(3674);e.exports=function(e,t){return e&&r(e,t,o)}},97786:(e,t,n)=>{var r=n(71811),o=n(40327);e.exports=function(e,t){for(var n=0,s=(t=r(t,e)).length;null!=e&&n{var r=n(62488),o=n(1469);e.exports=function(e,t,n){var s=t(e);return o(e)?s:r(s,n(e))}},44239:(e,t,n)=>{var r=n(62705),o=n(89607),s=n(2333),i=r?r.toStringTag:void 0;e.exports=function(e){return null==e?void 0===e?"[object Undefined]":"[object Null]":i&&i in Object(e)?o(e):s(e)}},13:e=>{e.exports=function(e,t){return null!=e&&t in Object(e)}},42118:(e,t,n)=>{var r=n(41848),o=n(62722),s=n(42351);e.exports=function(e,t,n){return t==t?s(e,t,n):r(e,o,n)}},9454:(e,t,n)=>{var r=n(44239),o=n(37005);e.exports=function(e){return o(e)&&"[object Arguments]"==r(e)}},90939:(e,t,n)=>{var r=n(2492),o=n(37005);e.exports=function e(t,n,s,i,a){return t===n||(null==t||null==n||!o(t)&&!o(n)?t!=t&&n!=n:r(t,n,s,i,e,a))}},2492:(e,t,n)=>{var r=n(46384),o=n(67114),s=n(18351),i=n(16096),a=n(98882),l=n(1469),c=n(44144),u=n(36719),p="[object Arguments]",h="[object Array]",f="[object Object]",d=Object.prototype.hasOwnProperty;e.exports=function(e,t,n,m,g,y){var v=l(e),b=l(t),w=v?h:a(e),E=b?h:a(t),x=(w=w==p?f:w)==f,S=(E=E==p?f:E)==f,_=w==E;if(_&&c(e)){if(!c(t))return!1;v=!0,x=!1}if(_&&!x)return y||(y=new r),v||u(e)?o(e,t,n,m,g,y):s(e,t,w,n,m,g,y);if(!(1&n)){var j=x&&d.call(e,"__wrapped__"),O=S&&d.call(t,"__wrapped__");if(j||O){var k=j?e.value():e,A=O?t.value():t;return y||(y=new r),g(k,A,n,m,y)}}return!!_&&(y||(y=new r),i(e,t,n,m,g,y))}},25588:(e,t,n)=>{var r=n(98882),o=n(37005);e.exports=function(e){return o(e)&&"[object Map]"==r(e)}},2958:(e,t,n)=>{var r=n(46384),o=n(90939);e.exports=function(e,t,n,s){var i=n.length,a=i,l=!s;if(null==e)return!a;for(e=Object(e);i--;){var c=n[i];if(l&&c[2]?c[1]!==e[c[0]]:!(c[0]in e))return!1}for(;++i{e.exports=function(e){return e!=e}},28458:(e,t,n)=>{var r=n(23560),o=n(15346),s=n(13218),i=n(80346),a=/^\[object .+?Constructor\]$/,l=Function.prototype,c=Object.prototype,u=l.toString,p=c.hasOwnProperty,h=RegExp("^"+u.call(p).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");e.exports=function(e){return!(!s(e)||o(e))&&(r(e)?h:a).test(i(e))}},29221:(e,t,n)=>{var r=n(98882),o=n(37005);e.exports=function(e){return o(e)&&"[object Set]"==r(e)}},38749:(e,t,n)=>{var r=n(44239),o=n(41780),s=n(37005),i={};i["[object Float32Array]"]=i["[object Float64Array]"]=i["[object Int8Array]"]=i["[object Int16Array]"]=i["[object Int32Array]"]=i["[object Uint8Array]"]=i["[object Uint8ClampedArray]"]=i["[object Uint16Array]"]=i["[object Uint32Array]"]=!0,i["[object Arguments]"]=i["[object Array]"]=i["[object ArrayBuffer]"]=i["[object Boolean]"]=i["[object DataView]"]=i["[object Date]"]=i["[object Error]"]=i["[object Function]"]=i["[object Map]"]=i["[object Number]"]=i["[object Object]"]=i["[object RegExp]"]=i["[object Set]"]=i["[object String]"]=i["[object WeakMap]"]=!1,e.exports=function(e){return s(e)&&o(e.length)&&!!i[r(e)]}},67206:(e,t,n)=>{var r=n(91573),o=n(16432),s=n(6557),i=n(1469),a=n(39601);e.exports=function(e){return"function"==typeof e?e:null==e?s:"object"==typeof e?i(e)?o(e[0],e[1]):r(e):a(e)}},280:(e,t,n)=>{var r=n(25726),o=n(86916),s=Object.prototype.hasOwnProperty;e.exports=function(e){if(!r(e))return o(e);var t=[];for(var n in Object(e))s.call(e,n)&&"constructor"!=n&&t.push(n);return t}},10313:(e,t,n)=>{var r=n(13218),o=n(25726),s=n(33498),i=Object.prototype.hasOwnProperty;e.exports=function(e){if(!r(e))return s(e);var t=o(e),n=[];for(var a in e)("constructor"!=a||!t&&i.call(e,a))&&n.push(a);return n}},9435:e=>{e.exports=function(){}},91573:(e,t,n)=>{var r=n(2958),o=n(1499),s=n(42634);e.exports=function(e){var t=o(e);return 1==t.length&&t[0][2]?s(t[0][0],t[0][1]):function(n){return n===e||r(n,e,t)}}},16432:(e,t,n)=>{var r=n(90939),o=n(27361),s=n(79095),i=n(15403),a=n(89162),l=n(42634),c=n(40327);e.exports=function(e,t){return i(e)&&a(t)?l(c(e),t):function(n){var i=o(n,e);return void 0===i&&i===t?s(n,e):r(t,i,3)}}},42980:(e,t,n)=>{var r=n(46384),o=n(86556),s=n(28483),i=n(59783),a=n(13218),l=n(81704),c=n(36390);e.exports=function e(t,n,u,p,h){t!==n&&s(n,(function(s,l){if(h||(h=new r),a(s))i(t,n,l,u,e,p,h);else{var f=p?p(c(t,l),s,l+"",t,n,h):void 0;void 0===f&&(f=s),o(t,l,f)}}),l)}},59783:(e,t,n)=>{var r=n(86556),o=n(64626),s=n(77133),i=n(278),a=n(38517),l=n(35694),c=n(1469),u=n(29246),p=n(44144),h=n(23560),f=n(13218),d=n(68630),m=n(36719),g=n(36390),y=n(59881);e.exports=function(e,t,n,v,b,w,E){var x=g(e,n),S=g(t,n),_=E.get(S);if(_)r(e,n,_);else{var j=w?w(x,S,n+"",e,t,E):void 0,O=void 0===j;if(O){var k=c(S),A=!k&&p(S),C=!k&&!A&&m(S);j=S,k||A||C?c(x)?j=x:u(x)?j=i(x):A?(O=!1,j=o(S,!0)):C?(O=!1,j=s(S,!0)):j=[]:d(S)||l(S)?(j=x,l(x)?j=y(x):f(x)&&!h(x)||(j=a(S))):O=!1}O&&(E.set(S,j),b(j,S,v,w,E),E.delete(S)),r(e,n,j)}}},40371:e=>{e.exports=function(e){return function(t){return null==t?void 0:t[e]}}},79152:(e,t,n)=>{var r=n(97786);e.exports=function(e){return function(t){return r(t,e)}}},18674:e=>{e.exports=function(e){return function(t){return null==e?void 0:e[t]}}},10107:e=>{e.exports=function(e,t,n,r,o){return o(e,(function(e,o,s){n=r?(r=!1,e):t(n,e,o,s)})),n}},5976:(e,t,n)=>{var r=n(6557),o=n(45357),s=n(30061);e.exports=function(e,t){return s(o(e,t,r),e+"")}},10611:(e,t,n)=>{var r=n(34865),o=n(71811),s=n(65776),i=n(13218),a=n(40327);e.exports=function(e,t,n,l){if(!i(e))return e;for(var c=-1,u=(t=o(t,e)).length,p=u-1,h=e;null!=h&&++c{var r=n(6557),o=n(89250),s=o?function(e,t){return o.set(e,t),e}:r;e.exports=s},56560:(e,t,n)=>{var r=n(75703),o=n(38777),s=n(6557),i=o?function(e,t){return o(e,"toString",{configurable:!0,enumerable:!1,value:r(t),writable:!0})}:s;e.exports=i},14259:e=>{e.exports=function(e,t,n){var r=-1,o=e.length;t<0&&(t=-t>o?0:o+t),(n=n>o?o:n)<0&&(n+=o),o=t>n?0:n-t>>>0,t>>>=0;for(var s=Array(o);++r{var r=n(89881);e.exports=function(e,t){var n;return r(e,(function(e,r,o){return!(n=t(e,r,o))})),!!n}},22545:e=>{e.exports=function(e,t){for(var n=-1,r=Array(e);++n{var r=n(62705),o=n(29932),s=n(1469),i=n(33448),a=r?r.prototype:void 0,l=a?a.toString:void 0;e.exports=function e(t){if("string"==typeof t)return t;if(s(t))return o(t,e)+"";if(i(t))return l?l.call(t):"";var n=t+"";return"0"==n&&1/t==-Infinity?"-0":n}},27561:(e,t,n)=>{var r=n(67990),o=/^\s+/;e.exports=function(e){return e?e.slice(0,r(e)+1).replace(o,""):e}},7518:e=>{e.exports=function(e){return function(t){return e(t)}}},57406:(e,t,n)=>{var r=n(71811),o=n(10928),s=n(40292),i=n(40327);e.exports=function(e,t){return t=r(t,e),null==(e=s(e,t))||delete e[i(o(t))]}},1757:e=>{e.exports=function(e,t,n){for(var r=-1,o=e.length,s=t.length,i={};++r{e.exports=function(e,t){return e.has(t)}},71811:(e,t,n)=>{var r=n(1469),o=n(15403),s=n(55514),i=n(79833);e.exports=function(e,t){return r(e)?e:o(e,t)?[e]:s(i(e))}},40180:(e,t,n)=>{var r=n(14259);e.exports=function(e,t,n){var o=e.length;return n=void 0===n?o:n,!t&&n>=o?e:r(e,t,n)}},74318:(e,t,n)=>{var r=n(11149);e.exports=function(e){var t=new e.constructor(e.byteLength);return new r(t).set(new r(e)),t}},64626:(e,t,n)=>{e=n.nmd(e);var r=n(55639),o=t&&!t.nodeType&&t,s=o&&e&&!e.nodeType&&e,i=s&&s.exports===o?r.Buffer:void 0,a=i?i.allocUnsafe:void 0;e.exports=function(e,t){if(t)return e.slice();var n=e.length,r=a?a(n):new e.constructor(n);return e.copy(r),r}},57157:(e,t,n)=>{var r=n(74318);e.exports=function(e,t){var n=t?r(e.buffer):e.buffer;return new e.constructor(n,e.byteOffset,e.byteLength)}},93147:e=>{var t=/\w*$/;e.exports=function(e){var n=new e.constructor(e.source,t.exec(e));return n.lastIndex=e.lastIndex,n}},40419:(e,t,n)=>{var r=n(62705),o=r?r.prototype:void 0,s=o?o.valueOf:void 0;e.exports=function(e){return s?Object(s.call(e)):{}}},77133:(e,t,n)=>{var r=n(74318);e.exports=function(e,t){var n=t?r(e.buffer):e.buffer;return new e.constructor(n,e.byteOffset,e.length)}},52157:e=>{var t=Math.max;e.exports=function(e,n,r,o){for(var s=-1,i=e.length,a=r.length,l=-1,c=n.length,u=t(i-a,0),p=Array(c+u),h=!o;++l{var t=Math.max;e.exports=function(e,n,r,o){for(var s=-1,i=e.length,a=-1,l=r.length,c=-1,u=n.length,p=t(i-l,0),h=Array(p+u),f=!o;++s{e.exports=function(e,t){var n=-1,r=e.length;for(t||(t=Array(r));++n{var r=n(34865),o=n(89465);e.exports=function(e,t,n,s){var i=!n;n||(n={});for(var a=-1,l=t.length;++a{var r=n(98363),o=n(99551);e.exports=function(e,t){return r(e,o(e),t)}},1911:(e,t,n)=>{var r=n(98363),o=n(51442);e.exports=function(e,t){return r(e,o(e),t)}},14429:(e,t,n)=>{var r=n(55639)["__core-js_shared__"];e.exports=r},97991:e=>{e.exports=function(e,t){for(var n=e.length,r=0;n--;)e[n]===t&&++r;return r}},21463:(e,t,n)=>{var r=n(5976),o=n(16612);e.exports=function(e){return r((function(t,n){var r=-1,s=n.length,i=s>1?n[s-1]:void 0,a=s>2?n[2]:void 0;for(i=e.length>3&&"function"==typeof i?(s--,i):void 0,a&&o(n[0],n[1],a)&&(i=s<3?void 0:i,s=1),t=Object(t);++r{var r=n(98612);e.exports=function(e,t){return function(n,o){if(null==n)return n;if(!r(n))return e(n,o);for(var s=n.length,i=t?s:-1,a=Object(n);(t?i--:++i{e.exports=function(e){return function(t,n,r){for(var o=-1,s=Object(t),i=r(t),a=i.length;a--;){var l=i[e?a:++o];if(!1===n(s[l],l,s))break}return t}}},22402:(e,t,n)=>{var r=n(71774),o=n(55639);e.exports=function(e,t,n){var s=1&t,i=r(e);return function t(){return(this&&this!==o&&this instanceof t?i:e).apply(s?n:this,arguments)}}},98805:(e,t,n)=>{var r=n(40180),o=n(62689),s=n(83140),i=n(79833);e.exports=function(e){return function(t){t=i(t);var n=o(t)?s(t):void 0,a=n?n[0]:t.charAt(0),l=n?r(n,1).join(""):t.slice(1);return a[e]()+l}}},35393:(e,t,n)=>{var r=n(62663),o=n(53816),s=n(58748),i=RegExp("['’]","g");e.exports=function(e){return function(t){return r(s(o(t).replace(i,"")),e,"")}}},71774:(e,t,n)=>{var r=n(3118),o=n(13218);e.exports=function(e){return function(){var t=arguments;switch(t.length){case 0:return new e;case 1:return new e(t[0]);case 2:return new e(t[0],t[1]);case 3:return new e(t[0],t[1],t[2]);case 4:return new e(t[0],t[1],t[2],t[3]);case 5:return new e(t[0],t[1],t[2],t[3],t[4]);case 6:return new e(t[0],t[1],t[2],t[3],t[4],t[5]);case 7:return new e(t[0],t[1],t[2],t[3],t[4],t[5],t[6])}var n=r(e.prototype),s=e.apply(n,t);return o(s)?s:n}}},46347:(e,t,n)=>{var r=n(96874),o=n(71774),s=n(86935),i=n(94487),a=n(20893),l=n(46460),c=n(55639);e.exports=function(e,t,n){var u=o(e);return function o(){for(var p=arguments.length,h=Array(p),f=p,d=a(o);f--;)h[f]=arguments[f];var m=p<3&&h[0]!==d&&h[p-1]!==d?[]:l(h,d);return(p-=m.length){var r=n(67206),o=n(98612),s=n(3674);e.exports=function(e){return function(t,n,i){var a=Object(t);if(!o(t)){var l=r(n,3);t=s(t),n=function(e){return l(a[e],e,a)}}var c=e(t,n,i);return c>-1?a[l?t[c]:c]:void 0}}},86935:(e,t,n)=>{var r=n(52157),o=n(14054),s=n(97991),i=n(71774),a=n(94487),l=n(20893),c=n(90451),u=n(46460),p=n(55639);e.exports=function e(t,n,h,f,d,m,g,y,v,b){var w=128&n,E=1&n,x=2&n,S=24&n,_=512&n,j=x?void 0:i(t);return function O(){for(var k=arguments.length,A=Array(k),C=k;C--;)A[C]=arguments[C];if(S)var P=l(O),N=s(A,P);if(f&&(A=r(A,f,d,S)),m&&(A=o(A,m,g,S)),k-=N,S&&k1&&A.reverse(),w&&v{var r=n(96874),o=n(71774),s=n(55639);e.exports=function(e,t,n,i){var a=1&t,l=o(e);return function t(){for(var o=-1,c=arguments.length,u=-1,p=i.length,h=Array(p+c),f=this&&this!==s&&this instanceof t?l:e;++u{var r=n(86528),o=n(258),s=n(69255);e.exports=function(e,t,n,i,a,l,c,u,p,h){var f=8&t;t|=f?32:64,4&(t&=~(f?64:32))||(t&=-4);var d=[e,t,a,f?l:void 0,f?c:void 0,f?void 0:l,f?void 0:c,u,p,h],m=n.apply(void 0,d);return r(e)&&o(m,d),m.placeholder=i,s(m,e,t)}},97727:(e,t,n)=>{var r=n(28045),o=n(22402),s=n(46347),i=n(86935),a=n(84375),l=n(66833),c=n(63833),u=n(258),p=n(69255),h=n(40554),f=Math.max;e.exports=function(e,t,n,d,m,g,y,v){var b=2&t;if(!b&&"function"!=typeof e)throw new TypeError("Expected a function");var w=d?d.length:0;if(w||(t&=-97,d=m=void 0),y=void 0===y?y:f(h(y),0),v=void 0===v?v:h(v),w-=m?m.length:0,64&t){var E=d,x=m;d=m=void 0}var S=b?void 0:l(e),_=[e,t,n,d,m,E,x,g,y,v];if(S&&c(_,S),e=_[0],t=_[1],n=_[2],d=_[3],m=_[4],!(v=_[9]=void 0===_[9]?b?0:e.length:f(_[9]-w,0))&&24&t&&(t&=-25),t&&1!=t)j=8==t||16==t?s(e,t,v):32!=t&&33!=t||m.length?i.apply(void 0,_):a(e,t,n,d);else var j=o(e,t,n);return p((S?r:u)(j,_),e,t)}},60696:(e,t,n)=>{var r=n(68630);e.exports=function(e){return r(e)?void 0:e}},69389:(e,t,n)=>{var r=n(18674)({À:"A",Ã:"A",Â:"A",Ã:"A",Ä:"A",Ã…:"A",à:"a",á:"a",â:"a",ã:"a",ä:"a",Ã¥:"a",Ç:"C",ç:"c",Ã:"D",ð:"d",È:"E",É:"E",Ê:"E",Ë:"E",è:"e",é:"e",ê:"e",ë:"e",ÃŒ:"I",Ã:"I",ÃŽ:"I",Ã:"I",ì:"i",í:"i",î:"i",ï:"i",Ñ:"N",ñ:"n",Ã’:"O",Ó:"O",Ô:"O",Õ:"O",Ö:"O",Ø:"O",ò:"o",ó:"o",ô:"o",õ:"o",ö:"o",ø:"o",Ù:"U",Ú:"U",Û:"U",Ü:"U",ù:"u",ú:"u",û:"u",ü:"u",Ã:"Y",ý:"y",ÿ:"y",Æ:"Ae",æ:"ae",Þ:"Th",þ:"th",ß:"ss",Ä€:"A",Ä‚:"A",Ä„:"A",Ä:"a",ă:"a",Ä…:"a",Ć:"C",Ĉ:"C",ÄŠ:"C",ÄŒ:"C",ć:"c",ĉ:"c",Ä‹:"c",Ä:"c",ÄŽ:"D",Ä:"D",Ä:"d",Ä‘:"d",Ä’:"E",Ä”:"E",Ä–:"E",Ę:"E",Äš:"E",Ä“:"e",Ä•:"e",Ä—:"e",Ä™:"e",Ä›:"e",Äœ:"G",Äž:"G",Ä :"G",Ä¢:"G",Ä:"g",ÄŸ:"g",Ä¡:"g",Ä£:"g",Ĥ:"H",Ħ:"H",Ä¥:"h",ħ:"h",Ĩ:"I",Ī:"I",Ĭ:"I",Ä®:"I",İ:"I",Ä©:"i",Ä«:"i",Ä­:"i",į:"i",ı:"i",Ä´:"J",ĵ:"j",Ķ:"K",Ä·:"k",ĸ:"k",Ĺ:"L",Ä»:"L",Ľ:"L",Ä¿:"L",Å:"L",ĺ:"l",ļ:"l",ľ:"l",Å€:"l",Å‚:"l",Ń:"N",Å…:"N",Ň:"N",ÅŠ:"N",Å„:"n",ņ:"n",ň:"n",Å‹:"n",ÅŒ:"O",ÅŽ:"O",Å:"O",Å:"o",Å:"o",Å‘:"o",Å”:"R",Å–:"R",Ř:"R",Å•:"r",Å—:"r",Å™:"r",Åš:"S",Åœ:"S",Åž:"S",Å :"S",Å›:"s",Å:"s",ÅŸ:"s",Å¡:"s",Å¢:"T",Ť:"T",Ŧ:"T",Å£:"t",Å¥:"t",ŧ:"t",Ũ:"U",Ū:"U",Ŭ:"U",Å®:"U",Ű:"U",Ų:"U",Å©:"u",Å«:"u",Å­:"u",ů:"u",ű:"u",ų:"u",Å´:"W",ŵ:"w",Ŷ:"Y",Å·:"y",Ÿ:"Y",Ź:"Z",Å»:"Z",Ž:"Z",ź:"z",ż:"z",ž:"z",IJ:"IJ",ij:"ij",Å’:"Oe",Å“:"oe",ʼn:"'n",Å¿:"s"});e.exports=r},38777:(e,t,n)=>{var r=n(10852),o=function(){try{var e=r(Object,"defineProperty");return e({},"",{}),e}catch(e){}}();e.exports=o},67114:(e,t,n)=>{var r=n(88668),o=n(82908),s=n(74757);e.exports=function(e,t,n,i,a,l){var c=1&n,u=e.length,p=t.length;if(u!=p&&!(c&&p>u))return!1;var h=l.get(e),f=l.get(t);if(h&&f)return h==t&&f==e;var d=-1,m=!0,g=2&n?new r:void 0;for(l.set(e,t),l.set(t,e);++d{var r=n(62705),o=n(11149),s=n(77813),i=n(67114),a=n(68776),l=n(21814),c=r?r.prototype:void 0,u=c?c.valueOf:void 0;e.exports=function(e,t,n,r,c,p,h){switch(n){case"[object DataView]":if(e.byteLength!=t.byteLength||e.byteOffset!=t.byteOffset)return!1;e=e.buffer,t=t.buffer;case"[object ArrayBuffer]":return!(e.byteLength!=t.byteLength||!p(new o(e),new o(t)));case"[object Boolean]":case"[object Date]":case"[object Number]":return s(+e,+t);case"[object Error]":return e.name==t.name&&e.message==t.message;case"[object RegExp]":case"[object String]":return e==t+"";case"[object Map]":var f=a;case"[object Set]":var d=1&r;if(f||(f=l),e.size!=t.size&&!d)return!1;var m=h.get(e);if(m)return m==t;r|=2,h.set(e,t);var g=i(f(e),f(t),r,c,p,h);return h.delete(e),g;case"[object Symbol]":if(u)return u.call(e)==u.call(t)}return!1}},16096:(e,t,n)=>{var r=n(58234),o=Object.prototype.hasOwnProperty;e.exports=function(e,t,n,s,i,a){var l=1&n,c=r(e),u=c.length;if(u!=r(t).length&&!l)return!1;for(var p=u;p--;){var h=c[p];if(!(l?h in t:o.call(t,h)))return!1}var f=a.get(e),d=a.get(t);if(f&&d)return f==t&&d==e;var m=!0;a.set(e,t),a.set(t,e);for(var g=l;++p{var r=n(85564),o=n(45357),s=n(30061);e.exports=function(e){return s(o(e,void 0,r),e+"")}},31957:(e,t,n)=>{var r="object"==typeof n.g&&n.g&&n.g.Object===Object&&n.g;e.exports=r},58234:(e,t,n)=>{var r=n(68866),o=n(99551),s=n(3674);e.exports=function(e){return r(e,s,o)}},46904:(e,t,n)=>{var r=n(68866),o=n(51442),s=n(81704);e.exports=function(e){return r(e,s,o)}},66833:(e,t,n)=>{var r=n(89250),o=n(50308),s=r?function(e){return r.get(e)}:o;e.exports=s},97658:(e,t,n)=>{var r=n(52060),o=Object.prototype.hasOwnProperty;e.exports=function(e){for(var t=e.name+"",n=r[t],s=o.call(r,t)?n.length:0;s--;){var i=n[s],a=i.func;if(null==a||a==e)return i.name}return t}},20893:e=>{e.exports=function(e){return e.placeholder}},45050:(e,t,n)=>{var r=n(37019);e.exports=function(e,t){var n=e.__data__;return r(t)?n["string"==typeof t?"string":"hash"]:n.map}},1499:(e,t,n)=>{var r=n(89162),o=n(3674);e.exports=function(e){for(var t=o(e),n=t.length;n--;){var s=t[n],i=e[s];t[n]=[s,i,r(i)]}return t}},10852:(e,t,n)=>{var r=n(28458),o=n(47801);e.exports=function(e,t){var n=o(e,t);return r(n)?n:void 0}},85924:(e,t,n)=>{var r=n(5569)(Object.getPrototypeOf,Object);e.exports=r},89607:(e,t,n)=>{var r=n(62705),o=Object.prototype,s=o.hasOwnProperty,i=o.toString,a=r?r.toStringTag:void 0;e.exports=function(e){var t=s.call(e,a),n=e[a];try{e[a]=void 0;var r=!0}catch(e){}var o=i.call(e);return r&&(t?e[a]=n:delete e[a]),o}},99551:(e,t,n)=>{var r=n(34963),o=n(70479),s=Object.prototype.propertyIsEnumerable,i=Object.getOwnPropertySymbols,a=i?function(e){return null==e?[]:(e=Object(e),r(i(e),(function(t){return s.call(e,t)})))}:o;e.exports=a},51442:(e,t,n)=>{var r=n(62488),o=n(85924),s=n(99551),i=n(70479),a=Object.getOwnPropertySymbols?function(e){for(var t=[];e;)r(t,s(e)),e=o(e);return t}:i;e.exports=a},98882:(e,t,n)=>{var r=n(18552),o=n(57071),s=n(53818),i=n(58525),a=n(70577),l=n(44239),c=n(80346),u="[object Map]",p="[object Promise]",h="[object Set]",f="[object WeakMap]",d="[object DataView]",m=c(r),g=c(o),y=c(s),v=c(i),b=c(a),w=l;(r&&w(new r(new ArrayBuffer(1)))!=d||o&&w(new o)!=u||s&&w(s.resolve())!=p||i&&w(new i)!=h||a&&w(new a)!=f)&&(w=function(e){var t=l(e),n="[object Object]"==t?e.constructor:void 0,r=n?c(n):"";if(r)switch(r){case m:return d;case g:return u;case y:return p;case v:return h;case b:return f}return t}),e.exports=w},47801:e=>{e.exports=function(e,t){return null==e?void 0:e[t]}},58775:e=>{var t=/\{\n\/\* \[wrapped with (.+)\] \*/,n=/,? & /;e.exports=function(e){var r=e.match(t);return r?r[1].split(n):[]}},222:(e,t,n)=>{var r=n(71811),o=n(35694),s=n(1469),i=n(65776),a=n(41780),l=n(40327);e.exports=function(e,t,n){for(var c=-1,u=(t=r(t,e)).length,p=!1;++c{var t=RegExp("[\\u200d\\ud800-\\udfff\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\ufe0e\\ufe0f]");e.exports=function(e){return t.test(e)}},93157:e=>{var t=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/;e.exports=function(e){return t.test(e)}},51789:(e,t,n)=>{var r=n(94536);e.exports=function(){this.__data__=r?r(null):{},this.size=0}},80401:e=>{e.exports=function(e){var t=this.has(e)&&delete this.__data__[e];return this.size-=t?1:0,t}},57667:(e,t,n)=>{var r=n(94536),o=Object.prototype.hasOwnProperty;e.exports=function(e){var t=this.__data__;if(r){var n=t[e];return"__lodash_hash_undefined__"===n?void 0:n}return o.call(t,e)?t[e]:void 0}},21327:(e,t,n)=>{var r=n(94536),o=Object.prototype.hasOwnProperty;e.exports=function(e){var t=this.__data__;return r?void 0!==t[e]:o.call(t,e)}},81866:(e,t,n)=>{var r=n(94536);e.exports=function(e,t){var n=this.__data__;return this.size+=this.has(e)?0:1,n[e]=r&&void 0===t?"__lodash_hash_undefined__":t,this}},43824:e=>{var t=Object.prototype.hasOwnProperty;e.exports=function(e){var n=e.length,r=new e.constructor(n);return n&&"string"==typeof e[0]&&t.call(e,"index")&&(r.index=e.index,r.input=e.input),r}},29148:(e,t,n)=>{var r=n(74318),o=n(57157),s=n(93147),i=n(40419),a=n(77133);e.exports=function(e,t,n){var l=e.constructor;switch(t){case"[object ArrayBuffer]":return r(e);case"[object Boolean]":case"[object Date]":return new l(+e);case"[object DataView]":return o(e,n);case"[object Float32Array]":case"[object Float64Array]":case"[object Int8Array]":case"[object Int16Array]":case"[object Int32Array]":case"[object Uint8Array]":case"[object Uint8ClampedArray]":case"[object Uint16Array]":case"[object Uint32Array]":return a(e,n);case"[object Map]":case"[object Set]":return new l;case"[object Number]":case"[object String]":return new l(e);case"[object RegExp]":return s(e);case"[object Symbol]":return i(e)}}},38517:(e,t,n)=>{var r=n(3118),o=n(85924),s=n(25726);e.exports=function(e){return"function"!=typeof e.constructor||s(e)?{}:r(o(e))}},83112:e=>{var t=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/;e.exports=function(e,n){var r=n.length;if(!r)return e;var o=r-1;return n[o]=(r>1?"& ":"")+n[o],n=n.join(r>2?", ":" "),e.replace(t,"{\n/* [wrapped with "+n+"] */\n")}},37285:(e,t,n)=>{var r=n(62705),o=n(35694),s=n(1469),i=r?r.isConcatSpreadable:void 0;e.exports=function(e){return s(e)||o(e)||!!(i&&e&&e[i])}},65776:e=>{var t=/^(?:0|[1-9]\d*)$/;e.exports=function(e,n){var r=typeof e;return!!(n=null==n?9007199254740991:n)&&("number"==r||"symbol"!=r&&t.test(e))&&e>-1&&e%1==0&&e{var r=n(77813),o=n(98612),s=n(65776),i=n(13218);e.exports=function(e,t,n){if(!i(n))return!1;var a=typeof t;return!!("number"==a?o(n)&&s(t,n.length):"string"==a&&t in n)&&r(n[t],e)}},15403:(e,t,n)=>{var r=n(1469),o=n(33448),s=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,i=/^\w*$/;e.exports=function(e,t){if(r(e))return!1;var n=typeof e;return!("number"!=n&&"symbol"!=n&&"boolean"!=n&&null!=e&&!o(e))||(i.test(e)||!s.test(e)||null!=t&&e in Object(t))}},37019:e=>{e.exports=function(e){var t=typeof e;return"string"==t||"number"==t||"symbol"==t||"boolean"==t?"__proto__"!==e:null===e}},86528:(e,t,n)=>{var r=n(96425),o=n(66833),s=n(97658),i=n(8111);e.exports=function(e){var t=s(e),n=i[t];if("function"!=typeof n||!(t in r.prototype))return!1;if(e===n)return!0;var a=o(n);return!!a&&e===a[0]}},15346:(e,t,n)=>{var r,o=n(14429),s=(r=/[^.]+$/.exec(o&&o.keys&&o.keys.IE_PROTO||""))?"Symbol(src)_1."+r:"";e.exports=function(e){return!!s&&s in e}},25726:e=>{var t=Object.prototype;e.exports=function(e){var n=e&&e.constructor;return e===("function"==typeof n&&n.prototype||t)}},89162:(e,t,n)=>{var r=n(13218);e.exports=function(e){return e==e&&!r(e)}},27040:e=>{e.exports=function(){this.__data__=[],this.size=0}},14125:(e,t,n)=>{var r=n(18470),o=Array.prototype.splice;e.exports=function(e){var t=this.__data__,n=r(t,e);return!(n<0)&&(n==t.length-1?t.pop():o.call(t,n,1),--this.size,!0)}},82117:(e,t,n)=>{var r=n(18470);e.exports=function(e){var t=this.__data__,n=r(t,e);return n<0?void 0:t[n][1]}},67518:(e,t,n)=>{var r=n(18470);e.exports=function(e){return r(this.__data__,e)>-1}},54705:(e,t,n)=>{var r=n(18470);e.exports=function(e,t){var n=this.__data__,o=r(n,e);return o<0?(++this.size,n.push([e,t])):n[o][1]=t,this}},24785:(e,t,n)=>{var r=n(1989),o=n(38407),s=n(57071);e.exports=function(){this.size=0,this.__data__={hash:new r,map:new(s||o),string:new r}}},11285:(e,t,n)=>{var r=n(45050);e.exports=function(e){var t=r(this,e).delete(e);return this.size-=t?1:0,t}},96e3:(e,t,n)=>{var r=n(45050);e.exports=function(e){return r(this,e).get(e)}},49916:(e,t,n)=>{var r=n(45050);e.exports=function(e){return r(this,e).has(e)}},95265:(e,t,n)=>{var r=n(45050);e.exports=function(e,t){var n=r(this,e),o=n.size;return n.set(e,t),this.size+=n.size==o?0:1,this}},68776:e=>{e.exports=function(e){var t=-1,n=Array(e.size);return e.forEach((function(e,r){n[++t]=[r,e]})),n}},42634:e=>{e.exports=function(e,t){return function(n){return null!=n&&(n[e]===t&&(void 0!==t||e in Object(n)))}}},24523:(e,t,n)=>{var r=n(88306);e.exports=function(e){var t=r(e,(function(e){return 500===n.size&&n.clear(),e})),n=t.cache;return t}},63833:(e,t,n)=>{var r=n(52157),o=n(14054),s=n(46460),i="__lodash_placeholder__",a=128,l=Math.min;e.exports=function(e,t){var n=e[1],c=t[1],u=n|c,p=u<131,h=c==a&&8==n||c==a&&256==n&&e[7].length<=t[8]||384==c&&t[7].length<=t[8]&&8==n;if(!p&&!h)return e;1&c&&(e[2]=t[2],u|=1&n?0:4);var f=t[3];if(f){var d=e[3];e[3]=d?r(d,f,t[4]):f,e[4]=d?s(e[3],i):t[4]}return(f=t[5])&&(d=e[5],e[5]=d?o(d,f,t[6]):f,e[6]=d?s(e[5],i):t[6]),(f=t[7])&&(e[7]=f),c&a&&(e[8]=null==e[8]?t[8]:l(e[8],t[8])),null==e[9]&&(e[9]=t[9]),e[0]=t[0],e[1]=u,e}},89250:(e,t,n)=>{var r=n(70577),o=r&&new r;e.exports=o},94536:(e,t,n)=>{var r=n(10852)(Object,"create");e.exports=r},86916:(e,t,n)=>{var r=n(5569)(Object.keys,Object);e.exports=r},33498:e=>{e.exports=function(e){var t=[];if(null!=e)for(var n in Object(e))t.push(n);return t}},31167:(e,t,n)=>{e=n.nmd(e);var r=n(31957),o=t&&!t.nodeType&&t,s=o&&e&&!e.nodeType&&e,i=s&&s.exports===o&&r.process,a=function(){try{var e=s&&s.require&&s.require("util").types;return e||i&&i.binding&&i.binding("util")}catch(e){}}();e.exports=a},2333:e=>{var t=Object.prototype.toString;e.exports=function(e){return t.call(e)}},5569:e=>{e.exports=function(e,t){return function(n){return e(t(n))}}},45357:(e,t,n)=>{var r=n(96874),o=Math.max;e.exports=function(e,t,n){return t=o(void 0===t?e.length-1:t,0),function(){for(var s=arguments,i=-1,a=o(s.length-t,0),l=Array(a);++i{var r=n(97786),o=n(14259);e.exports=function(e,t){return t.length<2?e:r(e,o(t,0,-1))}},52060:e=>{e.exports={}},90451:(e,t,n)=>{var r=n(278),o=n(65776),s=Math.min;e.exports=function(e,t){for(var n=e.length,i=s(t.length,n),a=r(e);i--;){var l=t[i];e[i]=o(l,n)?a[l]:void 0}return e}},46460:e=>{var t="__lodash_placeholder__";e.exports=function(e,n){for(var r=-1,o=e.length,s=0,i=[];++r{var r=n(31957),o="object"==typeof self&&self&&self.Object===Object&&self,s=r||o||Function("return this")();e.exports=s},36390:e=>{e.exports=function(e,t){if(("constructor"!==t||"function"!=typeof e[t])&&"__proto__"!=t)return e[t]}},90619:e=>{e.exports=function(e){return this.__data__.set(e,"__lodash_hash_undefined__"),this}},72385:e=>{e.exports=function(e){return this.__data__.has(e)}},258:(e,t,n)=>{var r=n(28045),o=n(21275)(r);e.exports=o},21814:e=>{e.exports=function(e){var t=-1,n=Array(e.size);return e.forEach((function(e){n[++t]=e})),n}},30061:(e,t,n)=>{var r=n(56560),o=n(21275)(r);e.exports=o},69255:(e,t,n)=>{var r=n(58775),o=n(83112),s=n(30061),i=n(87241);e.exports=function(e,t,n){var a=t+"";return s(e,o(a,i(r(a),n)))}},21275:e=>{var t=Date.now;e.exports=function(e){var n=0,r=0;return function(){var o=t(),s=16-(o-r);if(r=o,s>0){if(++n>=800)return arguments[0]}else n=0;return e.apply(void 0,arguments)}}},37465:(e,t,n)=>{var r=n(38407);e.exports=function(){this.__data__=new r,this.size=0}},63779:e=>{e.exports=function(e){var t=this.__data__,n=t.delete(e);return this.size=t.size,n}},67599:e=>{e.exports=function(e){return this.__data__.get(e)}},44758:e=>{e.exports=function(e){return this.__data__.has(e)}},34309:(e,t,n)=>{var r=n(38407),o=n(57071),s=n(83369);e.exports=function(e,t){var n=this.__data__;if(n instanceof r){var i=n.__data__;if(!o||i.length<199)return i.push([e,t]),this.size=++n.size,this;n=this.__data__=new s(i)}return n.set(e,t),this.size=n.size,this}},42351:e=>{e.exports=function(e,t,n){for(var r=n-1,o=e.length;++r{var r=n(44286),o=n(62689),s=n(676);e.exports=function(e){return o(e)?s(e):r(e)}},55514:(e,t,n)=>{var r=n(24523),o=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,s=/\\(\\)?/g,i=r((function(e){var t=[];return 46===e.charCodeAt(0)&&t.push(""),e.replace(o,(function(e,n,r,o){t.push(r?o.replace(s,"$1"):n||e)})),t}));e.exports=i},40327:(e,t,n)=>{var r=n(33448);e.exports=function(e){if("string"==typeof e||r(e))return e;var t=e+"";return"0"==t&&1/e==-Infinity?"-0":t}},80346:e=>{var t=Function.prototype.toString;e.exports=function(e){if(null!=e){try{return t.call(e)}catch(e){}try{return e+""}catch(e){}}return""}},67990:e=>{var t=/\s/;e.exports=function(e){for(var n=e.length;n--&&t.test(e.charAt(n)););return n}},676:e=>{var t="\\ud800-\\udfff",n="["+t+"]",r="[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]",o="\\ud83c[\\udffb-\\udfff]",s="[^"+t+"]",i="(?:\\ud83c[\\udde6-\\uddff]){2}",a="[\\ud800-\\udbff][\\udc00-\\udfff]",l="(?:"+r+"|"+o+")"+"?",c="[\\ufe0e\\ufe0f]?",u=c+l+("(?:\\u200d(?:"+[s,i,a].join("|")+")"+c+l+")*"),p="(?:"+[s+r+"?",r,i,a,n].join("|")+")",h=RegExp(o+"(?="+o+")|"+p+u,"g");e.exports=function(e){return e.match(h)||[]}},2757:e=>{var t="\\ud800-\\udfff",n="\\u2700-\\u27bf",r="a-z\\xdf-\\xf6\\xf8-\\xff",o="A-Z\\xc0-\\xd6\\xd8-\\xde",s="\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",i="["+s+"]",a="\\d+",l="["+n+"]",c="["+r+"]",u="[^"+t+s+a+n+r+o+"]",p="(?:\\ud83c[\\udde6-\\uddff]){2}",h="[\\ud800-\\udbff][\\udc00-\\udfff]",f="["+o+"]",d="(?:"+c+"|"+u+")",m="(?:"+f+"|"+u+")",g="(?:['’](?:d|ll|m|re|s|t|ve))?",y="(?:['’](?:D|LL|M|RE|S|T|VE))?",v="(?:[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]|\\ud83c[\\udffb-\\udfff])?",b="[\\ufe0e\\ufe0f]?",w=b+v+("(?:\\u200d(?:"+["[^"+t+"]",p,h].join("|")+")"+b+v+")*"),E="(?:"+[l,p,h].join("|")+")"+w,x=RegExp([f+"?"+c+"+"+g+"(?="+[i,f,"$"].join("|")+")",m+"+"+y+"(?="+[i,f+d,"$"].join("|")+")",f+"?"+d+"+"+g,f+"+"+y,"\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])","\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",a,E].join("|"),"g");e.exports=function(e){return e.match(x)||[]}},87241:(e,t,n)=>{var r=n(77412),o=n(47443),s=[["ary",128],["bind",1],["bindKey",2],["curry",8],["curryRight",16],["flip",512],["partial",32],["partialRight",64],["rearg",256]];e.exports=function(e,t){return r(s,(function(n){var r="_."+n[0];t&n[1]&&!o(e,r)&&e.push(r)})),e.sort()}},21913:(e,t,n)=>{var r=n(96425),o=n(7548),s=n(278);e.exports=function(e){if(e instanceof r)return e.clone();var t=new o(e.__wrapped__,e.__chain__);return t.__actions__=s(e.__actions__),t.__index__=e.__index__,t.__values__=e.__values__,t}},39514:(e,t,n)=>{var r=n(97727);e.exports=function(e,t,n){return t=n?void 0:t,t=e&&null==t?e.length:t,r(e,128,void 0,void 0,void 0,void 0,t)}},68929:(e,t,n)=>{var r=n(48403),o=n(35393)((function(e,t,n){return t=t.toLowerCase(),e+(n?r(t):t)}));e.exports=o},48403:(e,t,n)=>{var r=n(79833),o=n(11700);e.exports=function(e){return o(r(e).toLowerCase())}},66678:(e,t,n)=>{var r=n(85990);e.exports=function(e){return r(e,4)}},75703:e=>{e.exports=function(e){return function(){return e}}},40087:(e,t,n)=>{var r=n(97727);function o(e,t,n){var s=r(e,8,void 0,void 0,void 0,void 0,void 0,t=n?void 0:t);return s.placeholder=o.placeholder,s}o.placeholder={},e.exports=o},23279:(e,t,n)=>{var r=n(13218),o=n(7771),s=n(14841),i=Math.max,a=Math.min;e.exports=function(e,t,n){var l,c,u,p,h,f,d=0,m=!1,g=!1,y=!0;if("function"!=typeof e)throw new TypeError("Expected a function");function v(t){var n=l,r=c;return l=c=void 0,d=t,p=e.apply(r,n)}function b(e){var n=e-f;return void 0===f||n>=t||n<0||g&&e-d>=u}function w(){var e=o();if(b(e))return E(e);h=setTimeout(w,function(e){var n=t-(e-f);return g?a(n,u-(e-d)):n}(e))}function E(e){return h=void 0,y&&l?v(e):(l=c=void 0,p)}function x(){var e=o(),n=b(e);if(l=arguments,c=this,f=e,n){if(void 0===h)return function(e){return d=e,h=setTimeout(w,t),m?v(e):p}(f);if(g)return clearTimeout(h),h=setTimeout(w,t),v(f)}return void 0===h&&(h=setTimeout(w,t)),p}return t=s(t)||0,r(n)&&(m=!!n.leading,u=(g="maxWait"in n)?i(s(n.maxWait)||0,t):u,y="trailing"in n?!!n.trailing:y),x.cancel=function(){void 0!==h&&clearTimeout(h),d=0,l=f=c=h=void 0},x.flush=function(){return void 0===h?p:E(o())},x}},53816:(e,t,n)=>{var r=n(69389),o=n(79833),s=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,i=RegExp("[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]","g");e.exports=function(e){return(e=o(e))&&e.replace(s,r).replace(i,"")}},77813:e=>{e.exports=function(e,t){return e===t||e!=e&&t!=t}},13311:(e,t,n)=>{var r=n(67740)(n(30998));e.exports=r},30998:(e,t,n)=>{var r=n(41848),o=n(67206),s=n(40554),i=Math.max;e.exports=function(e,t,n){var a=null==e?0:e.length;if(!a)return-1;var l=null==n?0:s(n);return l<0&&(l=i(a+l,0)),r(e,o(t,3),l)}},85564:(e,t,n)=>{var r=n(21078);e.exports=function(e){return(null==e?0:e.length)?r(e,1):[]}},84599:(e,t,n)=>{var r=n(68836),o=n(69306),s=Array.prototype.push;function i(e,t){return 2==t?function(t,n){return e(t,n)}:function(t){return e(t)}}function a(e){for(var t=e?e.length:0,n=Array(t);t--;)n[t]=e[t];return n}function l(e,t){return function(){var n=arguments.length;if(n){for(var r=Array(n);n--;)r[n]=arguments[n];var o=r[0]=t.apply(void 0,r);return e.apply(void 0,r),o}}}e.exports=function e(t,n,c,u){var p="function"==typeof n,h=n===Object(n);if(h&&(u=c,c=n,n=void 0),null==c)throw new TypeError;u||(u={});var f={cap:!("cap"in u)||u.cap,curry:!("curry"in u)||u.curry,fixed:!("fixed"in u)||u.fixed,immutable:!("immutable"in u)||u.immutable,rearg:!("rearg"in u)||u.rearg},d=p?c:o,m="curry"in u&&u.curry,g="fixed"in u&&u.fixed,y="rearg"in u&&u.rearg,v=p?c.runInContext():void 0,b=p?c:{ary:t.ary,assign:t.assign,clone:t.clone,curry:t.curry,forEach:t.forEach,isArray:t.isArray,isError:t.isError,isFunction:t.isFunction,isWeakMap:t.isWeakMap,iteratee:t.iteratee,keys:t.keys,rearg:t.rearg,toInteger:t.toInteger,toPath:t.toPath},w=b.ary,E=b.assign,x=b.clone,S=b.curry,_=b.forEach,j=b.isArray,O=b.isError,k=b.isFunction,A=b.isWeakMap,C=b.keys,P=b.rearg,N=b.toInteger,I=b.toPath,T=C(r.aryMethod),R={castArray:function(e){return function(){var t=arguments[0];return j(t)?e(a(t)):e.apply(void 0,arguments)}},iteratee:function(e){return function(){var t=arguments[1],n=e(arguments[0],t),r=n.length;return f.cap&&"number"==typeof t?(t=t>2?t-2:1,r&&r<=t?n:i(n,t)):n}},mixin:function(e){return function(t){var n=this;if(!k(n))return e(n,Object(t));var r=[];return _(C(t),(function(e){k(t[e])&&r.push([e,n.prototype[e]])})),e(n,Object(t)),_(r,(function(e){var t=e[1];k(t)?n.prototype[e[0]]=t:delete n.prototype[e[0]]})),n}},nthArg:function(e){return function(t){var n=t<0?1:N(t)+1;return S(e(t),n)}},rearg:function(e){return function(t,n){var r=n?n.length:0;return S(e(t,n),r)}},runInContext:function(n){return function(r){return e(t,n(r),u)}}};function M(e,t){if(f.cap){var n=r.iterateeRearg[e];if(n)return function(e,t){return $(e,(function(e){var n=t.length;return function(e,t){return 2==t?function(t,n){return e.apply(void 0,arguments)}:function(t){return e.apply(void 0,arguments)}}(P(i(e,n),t),n)}))}(t,n);var o=!p&&r.iterateeAry[e];if(o)return function(e,t){return $(e,(function(e){return"function"==typeof e?i(e,t):e}))}(t,o)}return t}function D(e,t,n){if(f.fixed&&(g||!r.skipFixed[e])){var o=r.methodSpread[e],i=o&&o.start;return void 0===i?w(t,n):function(e,t){return function(){for(var n=arguments.length,r=n-1,o=Array(n);n--;)o[n]=arguments[n];var i=o[t],a=o.slice(0,t);return i&&s.apply(a,i),t!=r&&s.apply(a,o.slice(t+1)),e.apply(this,a)}}(t,i)}return t}function F(e,t,n){return f.rearg&&n>1&&(y||!r.skipRearg[e])?P(t,r.methodRearg[e]||r.aryRearg[n]):t}function L(e,t){for(var n=-1,r=(t=I(t)).length,o=r-1,s=x(Object(e)),i=s;null!=i&&++n1?S(t,n):t}(0,o=M(s,o),e),!1}})),!o})),o||(o=i),o==t&&(o=m?S(o,1):function(){return t.apply(this,arguments)}),o.convert=B(s,t),o.placeholder=t.placeholder=n,o}if(!h)return q(n,c,d);var U=c,z=[];return _(T,(function(e){_(r.aryMethod[e],(function(e){var t=U[r.remap[e]||e];t&&z.push([e,q(e,t,U)])}))})),_(C(U),(function(e){var t=U[e];if("function"==typeof t){for(var n=z.length;n--;)if(z[n][0]==e)return;t.convert=B(e,t),z.push([e,t])}})),_(z,(function(e){U[e[0]]=e[1]})),U.convert=function(e){return U.runInContext.convert(e)(void 0)},U.placeholder=U,_(C(U),(function(e){_(r.realToAlias[e]||[],(function(t){U[t]=U[e]}))})),U}},68836:(e,t)=>{t.aliasToReal={each:"forEach",eachRight:"forEachRight",entries:"toPairs",entriesIn:"toPairsIn",extend:"assignIn",extendAll:"assignInAll",extendAllWith:"assignInAllWith",extendWith:"assignInWith",first:"head",conforms:"conformsTo",matches:"isMatch",property:"get",__:"placeholder",F:"stubFalse",T:"stubTrue",all:"every",allPass:"overEvery",always:"constant",any:"some",anyPass:"overSome",apply:"spread",assoc:"set",assocPath:"set",complement:"negate",compose:"flowRight",contains:"includes",dissoc:"unset",dissocPath:"unset",dropLast:"dropRight",dropLastWhile:"dropRightWhile",equals:"isEqual",identical:"eq",indexBy:"keyBy",init:"initial",invertObj:"invert",juxt:"over",omitAll:"omit",nAry:"ary",path:"get",pathEq:"matchesProperty",pathOr:"getOr",paths:"at",pickAll:"pick",pipe:"flow",pluck:"map",prop:"get",propEq:"matchesProperty",propOr:"getOr",props:"at",symmetricDifference:"xor",symmetricDifferenceBy:"xorBy",symmetricDifferenceWith:"xorWith",takeLast:"takeRight",takeLastWhile:"takeRightWhile",unapply:"rest",unnest:"flatten",useWith:"overArgs",where:"conformsTo",whereEq:"isMatch",zipObj:"zipObject"},t.aryMethod={1:["assignAll","assignInAll","attempt","castArray","ceil","create","curry","curryRight","defaultsAll","defaultsDeepAll","floor","flow","flowRight","fromPairs","invert","iteratee","memoize","method","mergeAll","methodOf","mixin","nthArg","over","overEvery","overSome","rest","reverse","round","runInContext","spread","template","trim","trimEnd","trimStart","uniqueId","words","zipAll"],2:["add","after","ary","assign","assignAllWith","assignIn","assignInAllWith","at","before","bind","bindAll","bindKey","chunk","cloneDeepWith","cloneWith","concat","conformsTo","countBy","curryN","curryRightN","debounce","defaults","defaultsDeep","defaultTo","delay","difference","divide","drop","dropRight","dropRightWhile","dropWhile","endsWith","eq","every","filter","find","findIndex","findKey","findLast","findLastIndex","findLastKey","flatMap","flatMapDeep","flattenDepth","forEach","forEachRight","forIn","forInRight","forOwn","forOwnRight","get","groupBy","gt","gte","has","hasIn","includes","indexOf","intersection","invertBy","invoke","invokeMap","isEqual","isMatch","join","keyBy","lastIndexOf","lt","lte","map","mapKeys","mapValues","matchesProperty","maxBy","meanBy","merge","mergeAllWith","minBy","multiply","nth","omit","omitBy","overArgs","pad","padEnd","padStart","parseInt","partial","partialRight","partition","pick","pickBy","propertyOf","pull","pullAll","pullAt","random","range","rangeRight","rearg","reject","remove","repeat","restFrom","result","sampleSize","some","sortBy","sortedIndex","sortedIndexOf","sortedLastIndex","sortedLastIndexOf","sortedUniqBy","split","spreadFrom","startsWith","subtract","sumBy","take","takeRight","takeRightWhile","takeWhile","tap","throttle","thru","times","trimChars","trimCharsEnd","trimCharsStart","truncate","union","uniqBy","uniqWith","unset","unzipWith","without","wrap","xor","zip","zipObject","zipObjectDeep"],3:["assignInWith","assignWith","clamp","differenceBy","differenceWith","findFrom","findIndexFrom","findLastFrom","findLastIndexFrom","getOr","includesFrom","indexOfFrom","inRange","intersectionBy","intersectionWith","invokeArgs","invokeArgsMap","isEqualWith","isMatchWith","flatMapDepth","lastIndexOfFrom","mergeWith","orderBy","padChars","padCharsEnd","padCharsStart","pullAllBy","pullAllWith","rangeStep","rangeStepRight","reduce","reduceRight","replace","set","slice","sortedIndexBy","sortedLastIndexBy","transform","unionBy","unionWith","update","xorBy","xorWith","zipWith"],4:["fill","setWith","updateWith"]},t.aryRearg={2:[1,0],3:[2,0,1],4:[3,2,0,1]},t.iterateeAry={dropRightWhile:1,dropWhile:1,every:1,filter:1,find:1,findFrom:1,findIndex:1,findIndexFrom:1,findKey:1,findLast:1,findLastFrom:1,findLastIndex:1,findLastIndexFrom:1,findLastKey:1,flatMap:1,flatMapDeep:1,flatMapDepth:1,forEach:1,forEachRight:1,forIn:1,forInRight:1,forOwn:1,forOwnRight:1,map:1,mapKeys:1,mapValues:1,partition:1,reduce:2,reduceRight:2,reject:1,remove:1,some:1,takeRightWhile:1,takeWhile:1,times:1,transform:2},t.iterateeRearg={mapKeys:[1],reduceRight:[1,0]},t.methodRearg={assignInAllWith:[1,0],assignInWith:[1,2,0],assignAllWith:[1,0],assignWith:[1,2,0],differenceBy:[1,2,0],differenceWith:[1,2,0],getOr:[2,1,0],intersectionBy:[1,2,0],intersectionWith:[1,2,0],isEqualWith:[1,2,0],isMatchWith:[2,1,0],mergeAllWith:[1,0],mergeWith:[1,2,0],padChars:[2,1,0],padCharsEnd:[2,1,0],padCharsStart:[2,1,0],pullAllBy:[2,1,0],pullAllWith:[2,1,0],rangeStep:[1,2,0],rangeStepRight:[1,2,0],setWith:[3,1,2,0],sortedIndexBy:[2,1,0],sortedLastIndexBy:[2,1,0],unionBy:[1,2,0],unionWith:[1,2,0],updateWith:[3,1,2,0],xorBy:[1,2,0],xorWith:[1,2,0],zipWith:[1,2,0]},t.methodSpread={assignAll:{start:0},assignAllWith:{start:0},assignInAll:{start:0},assignInAllWith:{start:0},defaultsAll:{start:0},defaultsDeepAll:{start:0},invokeArgs:{start:2},invokeArgsMap:{start:2},mergeAll:{start:0},mergeAllWith:{start:0},partial:{start:1},partialRight:{start:1},without:{start:1},zipAll:{start:0}},t.mutate={array:{fill:!0,pull:!0,pullAll:!0,pullAllBy:!0,pullAllWith:!0,pullAt:!0,remove:!0,reverse:!0},object:{assign:!0,assignAll:!0,assignAllWith:!0,assignIn:!0,assignInAll:!0,assignInAllWith:!0,assignInWith:!0,assignWith:!0,defaults:!0,defaultsAll:!0,defaultsDeep:!0,defaultsDeepAll:!0,merge:!0,mergeAll:!0,mergeAllWith:!0,mergeWith:!0},set:{set:!0,setWith:!0,unset:!0,update:!0,updateWith:!0}},t.realToAlias=function(){var e=Object.prototype.hasOwnProperty,n=t.aliasToReal,r={};for(var o in n){var s=n[o];e.call(r,s)?r[s].push(o):r[s]=[o]}return r}(),t.remap={assignAll:"assign",assignAllWith:"assignWith",assignInAll:"assignIn",assignInAllWith:"assignInWith",curryN:"curry",curryRightN:"curryRight",defaultsAll:"defaults",defaultsDeepAll:"defaultsDeep",findFrom:"find",findIndexFrom:"findIndex",findLastFrom:"findLast",findLastIndexFrom:"findLastIndex",getOr:"get",includesFrom:"includes",indexOfFrom:"indexOf",invokeArgs:"invoke",invokeArgsMap:"invokeMap",lastIndexOfFrom:"lastIndexOf",mergeAll:"merge",mergeAllWith:"mergeWith",padChars:"pad",padCharsEnd:"padEnd",padCharsStart:"padStart",propertyOf:"get",rangeStep:"range",rangeStepRight:"rangeRight",restFrom:"rest",spreadFrom:"spread",trimChars:"trim",trimCharsEnd:"trimEnd",trimCharsStart:"trimStart",zipAll:"zip"},t.skipFixed={castArray:!0,flow:!0,flowRight:!0,iteratee:!0,mixin:!0,rearg:!0,runInContext:!0},t.skipRearg={add:!0,assign:!0,assignIn:!0,bind:!0,bindKey:!0,concat:!0,difference:!0,divide:!0,eq:!0,gt:!0,gte:!0,isEqual:!0,lt:!0,lte:!0,matchesProperty:!0,merge:!0,multiply:!0,overArgs:!0,partial:!0,partialRight:!0,propertyOf:!0,random:!0,range:!0,rangeRight:!0,subtract:!0,zip:!0,zipObject:!0,zipObjectDeep:!0}},4269:(e,t,n)=>{e.exports={ary:n(39514),assign:n(44037),clone:n(66678),curry:n(40087),forEach:n(77412),isArray:n(1469),isError:n(64647),isFunction:n(23560),isWeakMap:n(81018),iteratee:n(72594),keys:n(280),rearg:n(4963),toInteger:n(40554),toPath:n(30084)}},72700:(e,t,n)=>{e.exports=n(28252)},92822:(e,t,n)=>{var r=n(84599),o=n(4269);e.exports=function(e,t,n){return r(o,e,t,n)}},69306:e=>{e.exports={}},28252:(e,t,n)=>{var r=n(92822)("set",n(36968));r.placeholder=n(69306),e.exports=r},27361:(e,t,n)=>{var r=n(97786);e.exports=function(e,t,n){var o=null==e?void 0:r(e,t);return void 0===o?n:o}},79095:(e,t,n)=>{var r=n(13),o=n(222);e.exports=function(e,t){return null!=e&&o(e,t,r)}},6557:e=>{e.exports=function(e){return e}},35694:(e,t,n)=>{var r=n(9454),o=n(37005),s=Object.prototype,i=s.hasOwnProperty,a=s.propertyIsEnumerable,l=r(function(){return arguments}())?r:function(e){return o(e)&&i.call(e,"callee")&&!a.call(e,"callee")};e.exports=l},1469:e=>{var t=Array.isArray;e.exports=t},98612:(e,t,n)=>{var r=n(23560),o=n(41780);e.exports=function(e){return null!=e&&o(e.length)&&!r(e)}},29246:(e,t,n)=>{var r=n(98612),o=n(37005);e.exports=function(e){return o(e)&&r(e)}},51584:(e,t,n)=>{var r=n(44239),o=n(37005);e.exports=function(e){return!0===e||!1===e||o(e)&&"[object Boolean]"==r(e)}},44144:(e,t,n)=>{e=n.nmd(e);var r=n(55639),o=n(95062),s=t&&!t.nodeType&&t,i=s&&e&&!e.nodeType&&e,a=i&&i.exports===s?r.Buffer:void 0,l=(a?a.isBuffer:void 0)||o;e.exports=l},41609:(e,t,n)=>{var r=n(280),o=n(98882),s=n(35694),i=n(1469),a=n(98612),l=n(44144),c=n(25726),u=n(36719),p=Object.prototype.hasOwnProperty;e.exports=function(e){if(null==e)return!0;if(a(e)&&(i(e)||"string"==typeof e||"function"==typeof e.splice||l(e)||u(e)||s(e)))return!e.length;var t=o(e);if("[object Map]"==t||"[object Set]"==t)return!e.size;if(c(e))return!r(e).length;for(var n in e)if(p.call(e,n))return!1;return!0}},18446:(e,t,n)=>{var r=n(90939);e.exports=function(e,t){return r(e,t)}},64647:(e,t,n)=>{var r=n(44239),o=n(37005),s=n(68630);e.exports=function(e){if(!o(e))return!1;var t=r(e);return"[object Error]"==t||"[object DOMException]"==t||"string"==typeof e.message&&"string"==typeof e.name&&!s(e)}},23560:(e,t,n)=>{var r=n(44239),o=n(13218);e.exports=function(e){if(!o(e))return!1;var t=r(e);return"[object Function]"==t||"[object GeneratorFunction]"==t||"[object AsyncFunction]"==t||"[object Proxy]"==t}},41780:e=>{e.exports=function(e){return"number"==typeof e&&e>-1&&e%1==0&&e<=9007199254740991}},56688:(e,t,n)=>{var r=n(25588),o=n(7518),s=n(31167),i=s&&s.isMap,a=i?o(i):r;e.exports=a},45220:e=>{e.exports=function(e){return null===e}},81763:(e,t,n)=>{var r=n(44239),o=n(37005);e.exports=function(e){return"number"==typeof e||o(e)&&"[object Number]"==r(e)}},13218:e=>{e.exports=function(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}},37005:e=>{e.exports=function(e){return null!=e&&"object"==typeof e}},68630:(e,t,n)=>{var r=n(44239),o=n(85924),s=n(37005),i=Function.prototype,a=Object.prototype,l=i.toString,c=a.hasOwnProperty,u=l.call(Object);e.exports=function(e){if(!s(e)||"[object Object]"!=r(e))return!1;var t=o(e);if(null===t)return!0;var n=c.call(t,"constructor")&&t.constructor;return"function"==typeof n&&n instanceof n&&l.call(n)==u}},72928:(e,t,n)=>{var r=n(29221),o=n(7518),s=n(31167),i=s&&s.isSet,a=i?o(i):r;e.exports=a},47037:(e,t,n)=>{var r=n(44239),o=n(1469),s=n(37005);e.exports=function(e){return"string"==typeof e||!o(e)&&s(e)&&"[object String]"==r(e)}},33448:(e,t,n)=>{var r=n(44239),o=n(37005);e.exports=function(e){return"symbol"==typeof e||o(e)&&"[object Symbol]"==r(e)}},36719:(e,t,n)=>{var r=n(38749),o=n(7518),s=n(31167),i=s&&s.isTypedArray,a=i?o(i):r;e.exports=a},81018:(e,t,n)=>{var r=n(98882),o=n(37005);e.exports=function(e){return o(e)&&"[object WeakMap]"==r(e)}},72594:(e,t,n)=>{var r=n(85990),o=n(67206);e.exports=function(e){return o("function"==typeof e?e:r(e,1))}},3674:(e,t,n)=>{var r=n(14636),o=n(280),s=n(98612);e.exports=function(e){return s(e)?r(e):o(e)}},81704:(e,t,n)=>{var r=n(14636),o=n(10313),s=n(98612);e.exports=function(e){return s(e)?r(e,!0):o(e)}},10928:e=>{e.exports=function(e){var t=null==e?0:e.length;return t?e[t-1]:void 0}},88306:(e,t,n)=>{var r=n(83369);function o(e,t){if("function"!=typeof e||null!=t&&"function"!=typeof t)throw new TypeError("Expected a function");var n=function(){var r=arguments,o=t?t.apply(this,r):r[0],s=n.cache;if(s.has(o))return s.get(o);var i=e.apply(this,r);return n.cache=s.set(o,i)||s,i};return n.cache=new(o.Cache||r),n}o.Cache=r,e.exports=o},82492:(e,t,n)=>{var r=n(42980),o=n(21463)((function(e,t,n){r(e,t,n)}));e.exports=o},94885:e=>{e.exports=function(e){if("function"!=typeof e)throw new TypeError("Expected a function");return function(){var t=arguments;switch(t.length){case 0:return!e.call(this);case 1:return!e.call(this,t[0]);case 2:return!e.call(this,t[0],t[1]);case 3:return!e.call(this,t[0],t[1],t[2])}return!e.apply(this,t)}}},50308:e=>{e.exports=function(){}},7771:(e,t,n)=>{var r=n(55639);e.exports=function(){return r.Date.now()}},57557:(e,t,n)=>{var r=n(29932),o=n(85990),s=n(57406),i=n(71811),a=n(98363),l=n(60696),c=n(99021),u=n(46904),p=c((function(e,t){var n={};if(null==e)return n;var c=!1;t=r(t,(function(t){return t=i(t,e),c||(c=t.length>1),t})),a(e,u(e),n),c&&(n=o(n,7,l));for(var p=t.length;p--;)s(n,t[p]);return n}));e.exports=p},39601:(e,t,n)=>{var r=n(40371),o=n(79152),s=n(15403),i=n(40327);e.exports=function(e){return s(e)?r(i(e)):o(e)}},4963:(e,t,n)=>{var r=n(97727),o=n(99021),s=o((function(e,t){return r(e,256,void 0,void 0,void 0,t)}));e.exports=s},54061:(e,t,n)=>{var r=n(62663),o=n(89881),s=n(67206),i=n(10107),a=n(1469);e.exports=function(e,t,n){var l=a(e)?r:i,c=arguments.length<3;return l(e,s(t,4),n,c,o)}},36968:(e,t,n)=>{var r=n(10611);e.exports=function(e,t,n){return null==e?e:r(e,t,n)}},59704:(e,t,n)=>{var r=n(82908),o=n(67206),s=n(5076),i=n(1469),a=n(16612);e.exports=function(e,t,n){var l=i(e)?r:s;return n&&a(e,t,n)&&(t=void 0),l(e,o(t,3))}},70479:e=>{e.exports=function(){return[]}},95062:e=>{e.exports=function(){return!1}},18601:(e,t,n)=>{var r=n(14841),o=1/0;e.exports=function(e){return e?(e=r(e))===o||e===-1/0?17976931348623157e292*(e<0?-1:1):e==e?e:0:0===e?e:0}},40554:(e,t,n)=>{var r=n(18601);e.exports=function(e){var t=r(e),n=t%1;return t==t?n?t-n:t:0}},7334:(e,t,n)=>{var r=n(79833);e.exports=function(e){return r(e).toLowerCase()}},14841:(e,t,n)=>{var r=n(27561),o=n(13218),s=n(33448),i=/^[-+]0x[0-9a-f]+$/i,a=/^0b[01]+$/i,l=/^0o[0-7]+$/i,c=parseInt;e.exports=function(e){if("number"==typeof e)return e;if(s(e))return NaN;if(o(e)){var t="function"==typeof e.valueOf?e.valueOf():e;e=o(t)?t+"":t}if("string"!=typeof e)return 0===e?e:+e;e=r(e);var n=a.test(e);return n||l.test(e)?c(e.slice(2),n?2:8):i.test(e)?NaN:+e}},30084:(e,t,n)=>{var r=n(29932),o=n(278),s=n(1469),i=n(33448),a=n(55514),l=n(40327),c=n(79833);e.exports=function(e){return s(e)?r(e,l):i(e)?[e]:o(a(c(e)))}},59881:(e,t,n)=>{var r=n(98363),o=n(81704);e.exports=function(e){return r(e,o(e))}},79833:(e,t,n)=>{var r=n(80531);e.exports=function(e){return null==e?"":r(e)}},11700:(e,t,n)=>{var r=n(98805)("toUpperCase");e.exports=r},58748:(e,t,n)=>{var r=n(49029),o=n(93157),s=n(79833),i=n(2757);e.exports=function(e,t,n){return e=s(e),void 0===(t=n?void 0:t)?o(e)?i(e):r(e):e.match(t)||[]}},8111:(e,t,n)=>{var r=n(96425),o=n(7548),s=n(9435),i=n(1469),a=n(37005),l=n(21913),c=Object.prototype.hasOwnProperty;function u(e){if(a(e)&&!i(e)&&!(e instanceof r)){if(e instanceof o)return e;if(c.call(e,"__wrapped__"))return l(e)}return new o(e)}u.prototype=s.prototype,u.prototype.constructor=u,e.exports=u},7287:(e,t,n)=>{var r=n(34865),o=n(1757);e.exports=function(e,t){return o(e||[],t||[],r)}},96470:(e,t,n)=>{"use strict";var r=n(47802),o=n(21102);t.highlight=i,t.highlightAuto=function(e,t){var n,a,l,c,u=t||{},p=u.subset||r.listLanguages(),h=u.prefix,f=p.length,d=-1;null==h&&(h=s);if("string"!=typeof e)throw o("Expected `string` for value, got `%s`",e);a={relevance:0,language:null,value:[]},n={relevance:0,language:null,value:[]};for(;++da.relevance&&(a=l),l.relevance>n.relevance&&(a=n,n=l));a.language&&(n.secondBest=a);return n},t.registerLanguage=function(e,t){r.registerLanguage(e,t)},t.listLanguages=function(){return r.listLanguages()},t.registerAlias=function(e,t){var n,o=e;t&&((o={})[e]=t);for(n in o)r.registerAliases(o[n],{languageName:n})},a.prototype.addText=function(e){var t,n,r=this.stack;if(""===e)return;t=r[r.length-1],(n=t.children[t.children.length-1])&&"text"===n.type?n.value+=e:t.children.push({type:"text",value:e})},a.prototype.addKeyword=function(e,t){this.openNode(t),this.addText(e),this.closeNode()},a.prototype.addSublanguage=function(e,t){var n=this.stack,r=n[n.length-1],o=e.rootNode.children,s=t?{type:"element",tagName:"span",properties:{className:[t]},children:o}:o;r.children=r.children.concat(s)},a.prototype.openNode=function(e){var t=this.stack,n=this.options.classPrefix+e,r=t[t.length-1],o={type:"element",tagName:"span",properties:{className:[n]},children:[]};r.children.push(o),t.push(o)},a.prototype.closeNode=function(){this.stack.pop()},a.prototype.closeAllNodes=l,a.prototype.finalize=l,a.prototype.toHTML=function(){return""};var s="hljs-";function i(e,t,n){var i,l=r.configure({}),c=(n||{}).prefix;if("string"!=typeof e)throw o("Expected `string` for name, got `%s`",e);if(!r.getLanguage(e))throw o("Unknown language: `%s` is not registered",e);if("string"!=typeof t)throw o("Expected `string` for value, got `%s`",t);if(null==c&&(c=s),r.configure({__emitter:a,classPrefix:c}),i=r.highlight(t,{language:e,ignoreIllegals:!0}),r.configure(l||{}),i.errorRaised)throw i.errorRaised;return{relevance:i.relevance,language:i.language,value:i.emitter.rootNode.children}}function a(e){this.options=e,this.rootNode={children:[]},this.stack=[this.rootNode]}function l(){}},42566:(e,t,n)=>{const r=n(94885);function o(e){return"string"==typeof e?t=>t.element===e:e.constructor&&e.extend?t=>t instanceof e:e}class s{constructor(e){this.elements=e||[]}toValue(){return this.elements.map((e=>e.toValue()))}map(e,t){return this.elements.map(e,t)}flatMap(e,t){return this.map(e,t).reduce(((e,t)=>e.concat(t)),[])}compactMap(e,t){const n=[];return this.forEach((r=>{const o=e.bind(t)(r);o&&n.push(o)})),n}filter(e,t){return e=o(e),new s(this.elements.filter(e,t))}reject(e,t){return e=o(e),new s(this.elements.filter(r(e),t))}find(e,t){return e=o(e),this.elements.find(e,t)}forEach(e,t){this.elements.forEach(e,t)}reduce(e,t){return this.elements.reduce(e,t)}includes(e){return this.elements.some((t=>t.equals(e)))}shift(){return this.elements.shift()}unshift(e){this.elements.unshift(this.refract(e))}push(e){return this.elements.push(this.refract(e)),this}add(e){this.push(e)}get(e){return this.elements[e]}getValue(e){const t=this.elements[e];if(t)return t.toValue()}get length(){return this.elements.length}get isEmpty(){return 0===this.elements.length}get first(){return this.elements[0]}}"undefined"!=typeof Symbol&&(s.prototype[Symbol.iterator]=function(){return this.elements[Symbol.iterator]()}),e.exports=s},17645:e=>{class t{constructor(e,t){this.key=e,this.value=t}clone(){const e=new t;return this.key&&(e.key=this.key.clone()),this.value&&(e.value=this.value.clone()),e}}e.exports=t},78520:(e,t,n)=>{const r=n(45220),o=n(47037),s=n(81763),i=n(51584),a=n(13218),l=n(28219),c=n(99829);class u{constructor(e){this.elementMap={},this.elementDetection=[],this.Element=c.Element,this.KeyValuePair=c.KeyValuePair,e&&e.noDefault||this.useDefault(),this._attributeElementKeys=[],this._attributeElementArrayKeys=[]}use(e){return e.namespace&&e.namespace({base:this}),e.load&&e.load({base:this}),this}useDefault(){return this.register("null",c.NullElement).register("string",c.StringElement).register("number",c.NumberElement).register("boolean",c.BooleanElement).register("array",c.ArrayElement).register("object",c.ObjectElement).register("member",c.MemberElement).register("ref",c.RefElement).register("link",c.LinkElement),this.detect(r,c.NullElement,!1).detect(o,c.StringElement,!1).detect(s,c.NumberElement,!1).detect(i,c.BooleanElement,!1).detect(Array.isArray,c.ArrayElement,!1).detect(a,c.ObjectElement,!1),this}register(e,t){return this._elements=void 0,this.elementMap[e]=t,this}unregister(e){return this._elements=void 0,delete this.elementMap[e],this}detect(e,t,n){return void 0===n||n?this.elementDetection.unshift([e,t]):this.elementDetection.push([e,t]),this}toElement(e){if(e instanceof this.Element)return e;let t;for(let n=0;n{const t=e[0].toUpperCase()+e.substr(1);this._elements[t]=this.elementMap[e]}))),this._elements}get serialiser(){return new l(this)}}l.prototype.Namespace=u,e.exports=u},87526:(e,t,n)=>{const r=n(94885),o=n(42566);class s extends o{map(e,t){return this.elements.map((n=>e.bind(t)(n.value,n.key,n)))}filter(e,t){return new s(this.elements.filter((n=>e.bind(t)(n.value,n.key,n))))}reject(e,t){return this.filter(r(e.bind(t)))}forEach(e,t){return this.elements.forEach(((n,r)=>{e.bind(t)(n.value,n.key,n,r)}))}keys(){return this.map(((e,t)=>t.toValue()))}values(){return this.map((e=>e.toValue()))}}e.exports=s},99829:(e,t,n)=>{const r=n(3079),o=n(96295),s=n(16036),i=n(91090),a=n(18866),l=n(35804),c=n(5946),u=n(76735),p=n(59964),h=n(38588),f=n(42566),d=n(87526),m=n(17645);function g(e){if(e instanceof r)return e;if("string"==typeof e)return new s(e);if("number"==typeof e)return new i(e);if("boolean"==typeof e)return new a(e);if(null===e)return new o;if(Array.isArray(e))return new l(e.map(g));if("object"==typeof e){return new u(e)}return e}r.prototype.ObjectElement=u,r.prototype.RefElement=h,r.prototype.MemberElement=c,r.prototype.refract=g,f.prototype.refract=g,e.exports={Element:r,NullElement:o,StringElement:s,NumberElement:i,BooleanElement:a,ArrayElement:l,MemberElement:c,ObjectElement:u,LinkElement:p,RefElement:h,refract:g,ArraySlice:f,ObjectSlice:d,KeyValuePair:m}},59964:(e,t,n)=>{const r=n(3079);e.exports=class extends r{constructor(e,t,n){super(e||[],t,n),this.element="link"}get relation(){return this.attributes.get("relation")}set relation(e){this.attributes.set("relation",e)}get href(){return this.attributes.get("href")}set href(e){this.attributes.set("href",e)}}},38588:(e,t,n)=>{const r=n(3079);e.exports=class extends r{constructor(e,t,n){super(e||[],t,n),this.element="ref",this.path||(this.path="element")}get path(){return this.attributes.get("path")}set path(e){this.attributes.set("path",e)}}},43500:(e,t,n)=>{const r=n(78520),o=n(99829);t.lS=r,n(17645),t.O4=o.ArraySlice,o.ObjectSlice,t.W_=o.Element,t.RP=o.StringElement,t.VL=o.NumberElement,t.hh=o.BooleanElement,t.zr=o.NullElement,t.ON=o.ArrayElement,t.Sb=o.ObjectElement,t.c6=o.MemberElement,t.tK=o.RefElement,t.EA=o.LinkElement,t.Qc=o.refract,n(28219),n(3414)},35804:(e,t,n)=>{const r=n(94885),o=n(3079),s=n(42566);class i extends o{constructor(e,t,n){super(e||[],t,n),this.element="array"}primitive(){return"array"}get(e){return this.content[e]}getValue(e){const t=this.get(e);if(t)return t.toValue()}getIndex(e){return this.content[e]}set(e,t){return this.content[e]=this.refract(t),this}remove(e){const t=this.content.splice(e,1);return t.length?t[0]:null}map(e,t){return this.content.map(e,t)}flatMap(e,t){return this.map(e,t).reduce(((e,t)=>e.concat(t)),[])}compactMap(e,t){const n=[];return this.forEach((r=>{const o=e.bind(t)(r);o&&n.push(o)})),n}filter(e,t){return new s(this.content.filter(e,t))}reject(e,t){return this.filter(r(e),t)}reduce(e,t){let n,r;void 0!==t?(n=0,r=this.refract(t)):(n=1,r="object"===this.primitive()?this.first.value:this.first);for(let t=n;t{e.bind(t)(n,this.refract(r))}))}shift(){return this.content.shift()}unshift(e){this.content.unshift(this.refract(e))}push(e){return this.content.push(this.refract(e)),this}add(e){this.push(e)}findElements(e,t){const n=t||{},r=!!n.recursive,o=void 0===n.results?[]:n.results;return this.forEach(((t,n,s)=>{r&&void 0!==t.findElements&&t.findElements(e,{results:o,recursive:r}),e(t,n,s)&&o.push(t)})),o}find(e){return new s(this.findElements(e,{recursive:!0}))}findByElement(e){return this.find((t=>t.element===e))}findByClass(e){return this.find((t=>t.classes.includes(e)))}getById(e){return this.find((t=>t.id.toValue()===e)).first}includes(e){return this.content.some((t=>t.equals(e)))}contains(e){return this.includes(e)}empty(){return new this.constructor([])}"fantasy-land/empty"(){return this.empty()}concat(e){return new this.constructor(this.content.concat(e.content))}"fantasy-land/concat"(e){return this.concat(e)}"fantasy-land/map"(e){return new this.constructor(this.map(e))}"fantasy-land/chain"(e){return this.map((t=>e(t)),this).reduce(((e,t)=>e.concat(t)),this.empty())}"fantasy-land/filter"(e){return new this.constructor(this.content.filter(e))}"fantasy-land/reduce"(e,t){return this.content.reduce(e,t)}get length(){return this.content.length}get isEmpty(){return 0===this.content.length}get first(){return this.getIndex(0)}get second(){return this.getIndex(1)}get last(){return this.getIndex(this.length-1)}}i.empty=function(){return new this},i["fantasy-land/empty"]=i.empty,"undefined"!=typeof Symbol&&(i.prototype[Symbol.iterator]=function(){return this.content[Symbol.iterator]()}),e.exports=i},18866:(e,t,n)=>{const r=n(3079);e.exports=class extends r{constructor(e,t,n){super(e,t,n),this.element="boolean"}primitive(){return"boolean"}}},3079:(e,t,n)=>{const r=n(18446),o=n(17645),s=n(42566);class i{constructor(e,t,n){t&&(this.meta=t),n&&(this.attributes=n),this.content=e}freeze(){Object.isFrozen(this)||(this._meta&&(this.meta.parent=this,this.meta.freeze()),this._attributes&&(this.attributes.parent=this,this.attributes.freeze()),this.children.forEach((e=>{e.parent=this,e.freeze()}),this),this.content&&Array.isArray(this.content)&&Object.freeze(this.content),Object.freeze(this))}primitive(){}clone(){const e=new this.constructor;return e.element=this.element,this.meta.length&&(e._meta=this.meta.clone()),this.attributes.length&&(e._attributes=this.attributes.clone()),this.content?this.content.clone?e.content=this.content.clone():Array.isArray(this.content)?e.content=this.content.map((e=>e.clone())):e.content=this.content:e.content=this.content,e}toValue(){return this.content instanceof i?this.content.toValue():this.content instanceof o?{key:this.content.key.toValue(),value:this.content.value?this.content.value.toValue():void 0}:this.content&&this.content.map?this.content.map((e=>e.toValue()),this):this.content}toRef(e){if(""===this.id.toValue())throw Error("Cannot create reference to an element that does not contain an ID");const t=new this.RefElement(this.id.toValue());return e&&(t.path=e),t}findRecursive(...e){if(arguments.length>1&&!this.isFrozen)throw new Error("Cannot find recursive with multiple element names without first freezing the element. Call `element.freeze()`");const t=e.pop();let n=new s;const r=(e,t)=>(e.push(t),e),i=(e,n)=>{n.element===t&&e.push(n);const s=n.findRecursive(t);return s&&s.reduce(r,e),n.content instanceof o&&(n.content.key&&i(e,n.content.key),n.content.value&&i(e,n.content.value)),e};return this.content&&(this.content.element&&i(n,this.content),Array.isArray(this.content)&&this.content.reduce(i,n)),e.isEmpty||(n=n.filter((t=>{let n=t.parents.map((e=>e.element));for(const t in e){const r=e[t],o=n.indexOf(r);if(-1===o)return!1;n=n.splice(0,o)}return!0}))),n}set(e){return this.content=e,this}equals(e){return r(this.toValue(),e)}getMetaProperty(e,t){if(!this.meta.hasKey(e)){if(this.isFrozen){const e=this.refract(t);return e.freeze(),e}this.meta.set(e,t)}return this.meta.get(e)}setMetaProperty(e,t){this.meta.set(e,t)}get element(){return this._storedElement||"element"}set element(e){this._storedElement=e}get content(){return this._content}set content(e){if(e instanceof i)this._content=e;else if(e instanceof s)this.content=e.elements;else if("string"==typeof e||"number"==typeof e||"boolean"==typeof e||"null"===e||null==e)this._content=e;else if(e instanceof o)this._content=e;else if(Array.isArray(e))this._content=e.map(this.refract);else{if("object"!=typeof e)throw new Error("Cannot set content to given value");this._content=Object.keys(e).map((t=>new this.MemberElement(t,e[t])))}}get meta(){if(!this._meta){if(this.isFrozen){const e=new this.ObjectElement;return e.freeze(),e}this._meta=new this.ObjectElement}return this._meta}set meta(e){e instanceof this.ObjectElement?this._meta=e:this.meta.set(e||{})}get attributes(){if(!this._attributes){if(this.isFrozen){const e=new this.ObjectElement;return e.freeze(),e}this._attributes=new this.ObjectElement}return this._attributes}set attributes(e){e instanceof this.ObjectElement?this._attributes=e:this.attributes.set(e||{})}get id(){return this.getMetaProperty("id","")}set id(e){this.setMetaProperty("id",e)}get classes(){return this.getMetaProperty("classes",[])}set classes(e){this.setMetaProperty("classes",e)}get title(){return this.getMetaProperty("title","")}set title(e){this.setMetaProperty("title",e)}get description(){return this.getMetaProperty("description","")}set description(e){this.setMetaProperty("description",e)}get links(){return this.getMetaProperty("links",[])}set links(e){this.setMetaProperty("links",e)}get isFrozen(){return Object.isFrozen(this)}get parents(){let{parent:e}=this;const t=new s;for(;e;)t.push(e),e=e.parent;return t}get children(){if(Array.isArray(this.content))return new s(this.content);if(this.content instanceof o){const e=new s([this.content.key]);return this.content.value&&e.push(this.content.value),e}return this.content instanceof i?new s([this.content]):new s}get recursiveChildren(){const e=new s;return this.children.forEach((t=>{e.push(t),t.recursiveChildren.forEach((t=>{e.push(t)}))})),e}}e.exports=i},5946:(e,t,n)=>{const r=n(17645),o=n(3079);e.exports=class extends o{constructor(e,t,n,o){super(new r,n,o),this.element="member",this.key=e,this.value=t}get key(){return this.content.key}set key(e){this.content.key=this.refract(e)}get value(){return this.content.value}set value(e){this.content.value=this.refract(e)}}},96295:(e,t,n)=>{const r=n(3079);e.exports=class extends r{constructor(e,t,n){super(e||null,t,n),this.element="null"}primitive(){return"null"}set(){return new Error("Cannot set the value of null")}}},91090:(e,t,n)=>{const r=n(3079);e.exports=class extends r{constructor(e,t,n){super(e,t,n),this.element="number"}primitive(){return"number"}}},76735:(e,t,n)=>{const r=n(94885),o=n(13218),s=n(35804),i=n(5946),a=n(87526);e.exports=class extends s{constructor(e,t,n){super(e||[],t,n),this.element="object"}primitive(){return"object"}toValue(){return this.content.reduce(((e,t)=>(e[t.key.toValue()]=t.value?t.value.toValue():void 0,e)),{})}get(e){const t=this.getMember(e);if(t)return t.value}getMember(e){if(void 0!==e)return this.content.find((t=>t.key.toValue()===e))}remove(e){let t=null;return this.content=this.content.filter((n=>n.key.toValue()!==e||(t=n,!1))),t}getKey(e){const t=this.getMember(e);if(t)return t.key}set(e,t){if(o(e))return Object.keys(e).forEach((t=>{this.set(t,e[t])})),this;const n=e,r=this.getMember(n);return r?r.value=t:this.content.push(new i(n,t)),this}keys(){return this.content.map((e=>e.key.toValue()))}values(){return this.content.map((e=>e.value.toValue()))}hasKey(e){return this.content.some((t=>t.key.equals(e)))}items(){return this.content.map((e=>[e.key.toValue(),e.value.toValue()]))}map(e,t){return this.content.map((n=>e.bind(t)(n.value,n.key,n)))}compactMap(e,t){const n=[];return this.forEach(((r,o,s)=>{const i=e.bind(t)(r,o,s);i&&n.push(i)})),n}filter(e,t){return new a(this.content).filter(e,t)}reject(e,t){return this.filter(r(e),t)}forEach(e,t){return this.content.forEach((n=>e.bind(t)(n.value,n.key,n)))}}},16036:(e,t,n)=>{const r=n(3079);e.exports=class extends r{constructor(e,t,n){super(e,t,n),this.element="string"}primitive(){return"string"}get length(){return this.content.length}}},3414:(e,t,n)=>{const r=n(28219);e.exports=class extends r{serialise(e){if(!(e instanceof this.namespace.elements.Element))throw new TypeError(`Given element \`${e}\` is not an Element instance`);let t;e._attributes&&e.attributes.get("variable")&&(t=e.attributes.get("variable"));const n={element:e.element};e._meta&&e._meta.length>0&&(n.meta=this.serialiseObject(e.meta));const r="enum"===e.element||-1!==e.attributes.keys().indexOf("enumerations");if(r){const t=this.enumSerialiseAttributes(e);t&&(n.attributes=t)}else if(e._attributes&&e._attributes.length>0){let{attributes:r}=e;r.get("metadata")&&(r=r.clone(),r.set("meta",r.get("metadata")),r.remove("metadata")),"member"===e.element&&t&&(r=r.clone(),r.remove("variable")),r.length>0&&(n.attributes=this.serialiseObject(r))}if(r)n.content=this.enumSerialiseContent(e,n);else if(this[`${e.element}SerialiseContent`])n.content=this[`${e.element}SerialiseContent`](e,n);else if(void 0!==e.content){let r;t&&e.content.key?(r=e.content.clone(),r.key.attributes.set("variable",t),r=this.serialiseContent(r)):r=this.serialiseContent(e.content),this.shouldSerialiseContent(e,r)&&(n.content=r)}else this.shouldSerialiseContent(e,e.content)&&e instanceof this.namespace.elements.Array&&(n.content=[]);return n}shouldSerialiseContent(e,t){return"parseResult"===e.element||"httpRequest"===e.element||"httpResponse"===e.element||"category"===e.element||"link"===e.element||void 0!==t&&(!Array.isArray(t)||0!==t.length)}refSerialiseContent(e,t){return delete t.attributes,{href:e.toValue(),path:e.path.toValue()}}sourceMapSerialiseContent(e){return e.toValue()}dataStructureSerialiseContent(e){return[this.serialiseContent(e.content)]}enumSerialiseAttributes(e){const t=e.attributes.clone(),n=t.remove("enumerations")||new this.namespace.elements.Array([]),r=t.get("default");let o=t.get("samples")||new this.namespace.elements.Array([]);if(r&&r.content&&(r.content.attributes&&r.content.attributes.remove("typeAttributes"),t.set("default",new this.namespace.elements.Array([r.content]))),o.forEach((e=>{e.content&&e.content.element&&e.content.attributes.remove("typeAttributes")})),e.content&&0!==n.length&&o.unshift(e.content),o=o.map((e=>e instanceof this.namespace.elements.Array?[e]:new this.namespace.elements.Array([e.content]))),o.length&&t.set("samples",o),t.length>0)return this.serialiseObject(t)}enumSerialiseContent(e){if(e._attributes){const t=e.attributes.get("enumerations");if(t&&t.length>0)return t.content.map((e=>{const t=e.clone();return t.attributes.remove("typeAttributes"),this.serialise(t)}))}if(e.content){const t=e.content.clone();return t.attributes.remove("typeAttributes"),[this.serialise(t)]}return[]}deserialise(e){if("string"==typeof e)return new this.namespace.elements.String(e);if("number"==typeof e)return new this.namespace.elements.Number(e);if("boolean"==typeof e)return new this.namespace.elements.Boolean(e);if(null===e)return new this.namespace.elements.Null;if(Array.isArray(e))return new this.namespace.elements.Array(e.map(this.deserialise,this));const t=this.namespace.getElementClass(e.element),n=new t;n.element!==e.element&&(n.element=e.element),e.meta&&this.deserialiseObject(e.meta,n.meta),e.attributes&&this.deserialiseObject(e.attributes,n.attributes);const r=this.deserialiseContent(e.content);if(void 0===r&&null!==n.content||(n.content=r),"enum"===n.element){n.content&&n.attributes.set("enumerations",n.content);let e=n.attributes.get("samples");if(n.attributes.remove("samples"),e){const r=e;e=new this.namespace.elements.Array,r.forEach((r=>{r.forEach((r=>{const o=new t(r);o.element=n.element,e.push(o)}))}));const o=e.shift();n.content=o?o.content:void 0,n.attributes.set("samples",e)}else n.content=void 0;let r=n.attributes.get("default");if(r&&r.length>0){r=r.get(0);const e=new t(r);e.element=n.element,n.attributes.set("default",e)}}else if("dataStructure"===n.element&&Array.isArray(n.content))[n.content]=n.content;else if("category"===n.element){const e=n.attributes.get("meta");e&&(n.attributes.set("metadata",e),n.attributes.remove("meta"))}else"member"===n.element&&n.key&&n.key._attributes&&n.key._attributes.getValue("variable")&&(n.attributes.set("variable",n.key.attributes.get("variable")),n.key.attributes.remove("variable"));return n}serialiseContent(e){if(e instanceof this.namespace.elements.Element)return this.serialise(e);if(e instanceof this.namespace.KeyValuePair){const t={key:this.serialise(e.key)};return e.value&&(t.value=this.serialise(e.value)),t}return e&&e.map?e.map(this.serialise,this):e}deserialiseContent(e){if(e){if(e.element)return this.deserialise(e);if(e.key){const t=new this.namespace.KeyValuePair(this.deserialise(e.key));return e.value&&(t.value=this.deserialise(e.value)),t}if(e.map)return e.map(this.deserialise,this)}return e}shouldRefract(e){return!!(e._attributes&&e.attributes.keys().length||e._meta&&e.meta.keys().length)||"enum"!==e.element&&(e.element!==e.primitive()||"member"===e.element)}convertKeyToRefract(e,t){return this.shouldRefract(t)?this.serialise(t):"enum"===t.element?this.serialiseEnum(t):"array"===t.element?t.map((t=>this.shouldRefract(t)||"default"===e?this.serialise(t):"array"===t.element||"object"===t.element||"enum"===t.element?t.children.map((e=>this.serialise(e))):t.toValue())):"object"===t.element?(t.content||[]).map(this.serialise,this):t.toValue()}serialiseEnum(e){return e.children.map((e=>this.serialise(e)))}serialiseObject(e){const t={};return e.forEach(((e,n)=>{if(e){const r=n.toValue();t[r]=this.convertKeyToRefract(r,e)}})),t}deserialiseObject(e,t){Object.keys(e).forEach((n=>{t.set(n,this.deserialise(e[n]))}))}}},28219:e=>{e.exports=class{constructor(e){this.namespace=e||new this.Namespace}serialise(e){if(!(e instanceof this.namespace.elements.Element))throw new TypeError(`Given element \`${e}\` is not an Element instance`);const t={element:e.element};e._meta&&e._meta.length>0&&(t.meta=this.serialiseObject(e.meta)),e._attributes&&e._attributes.length>0&&(t.attributes=this.serialiseObject(e.attributes));const n=this.serialiseContent(e.content);return void 0!==n&&(t.content=n),t}deserialise(e){if(!e.element)throw new Error("Given value is not an object containing an element name");const t=new(this.namespace.getElementClass(e.element));t.element!==e.element&&(t.element=e.element),e.meta&&this.deserialiseObject(e.meta,t.meta),e.attributes&&this.deserialiseObject(e.attributes,t.attributes);const n=this.deserialiseContent(e.content);return void 0===n&&null!==t.content||(t.content=n),t}serialiseContent(e){if(e instanceof this.namespace.elements.Element)return this.serialise(e);if(e instanceof this.namespace.KeyValuePair){const t={key:this.serialise(e.key)};return e.value&&(t.value=this.serialise(e.value)),t}if(e&&e.map){if(0===e.length)return;return e.map(this.serialise,this)}return e}deserialiseContent(e){if(e){if(e.element)return this.deserialise(e);if(e.key){const t=new this.namespace.KeyValuePair(this.deserialise(e.key));return e.value&&(t.value=this.deserialise(e.value)),t}if(e.map)return e.map(this.deserialise,this)}return e}serialiseObject(e){const t={};if(e.forEach(((e,n)=>{e&&(t[n.toValue()]=this.serialise(e))})),0!==Object.keys(t).length)return t}deserialiseObject(e,t){Object.keys(e).forEach((n=>{t.set(n,this.deserialise(e[n]))}))}}},27418:e=>{"use strict";var t=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,r=Object.prototype.propertyIsEnumerable;e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;if("0123456789"!==Object.getOwnPropertyNames(t).map((function(e){return t[e]})).join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach((function(e){r[e]=e})),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(e){return!1}}()?Object.assign:function(e,o){for(var s,i,a=function(e){if(null==e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}(e),l=1;l{var r="function"==typeof Map&&Map.prototype,o=Object.getOwnPropertyDescriptor&&r?Object.getOwnPropertyDescriptor(Map.prototype,"size"):null,s=r&&o&&"function"==typeof o.get?o.get:null,i=r&&Map.prototype.forEach,a="function"==typeof Set&&Set.prototype,l=Object.getOwnPropertyDescriptor&&a?Object.getOwnPropertyDescriptor(Set.prototype,"size"):null,c=a&&l&&"function"==typeof l.get?l.get:null,u=a&&Set.prototype.forEach,p="function"==typeof WeakMap&&WeakMap.prototype?WeakMap.prototype.has:null,h="function"==typeof WeakSet&&WeakSet.prototype?WeakSet.prototype.has:null,f="function"==typeof WeakRef&&WeakRef.prototype?WeakRef.prototype.deref:null,d=Boolean.prototype.valueOf,m=Object.prototype.toString,g=Function.prototype.toString,y=String.prototype.match,v=String.prototype.slice,b=String.prototype.replace,w=String.prototype.toUpperCase,E=String.prototype.toLowerCase,x=RegExp.prototype.test,S=Array.prototype.concat,_=Array.prototype.join,j=Array.prototype.slice,O=Math.floor,k="function"==typeof BigInt?BigInt.prototype.valueOf:null,A=Object.getOwnPropertySymbols,C="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?Symbol.prototype.toString:null,P="function"==typeof Symbol&&"object"==typeof Symbol.iterator,N="function"==typeof Symbol&&Symbol.toStringTag&&(typeof Symbol.toStringTag===P||"symbol")?Symbol.toStringTag:null,I=Object.prototype.propertyIsEnumerable,T=("function"==typeof Reflect?Reflect.getPrototypeOf:Object.getPrototypeOf)||([].__proto__===Array.prototype?function(e){return e.__proto__}:null);function R(e,t){if(e===1/0||e===-1/0||e!=e||e&&e>-1e3&&e<1e3||x.call(/e/,t))return t;var n=/[0-9](?=(?:[0-9]{3})+(?![0-9]))/g;if("number"==typeof e){var r=e<0?-O(-e):O(e);if(r!==e){var o=String(r),s=v.call(t,o.length+1);return b.call(o,n,"$&_")+"."+b.call(b.call(s,/([0-9]{3})/g,"$&_"),/_$/,"")}}return b.call(t,n,"$&_")}var M=n(24654),D=M.custom,F=U(D)?D:null;function L(e,t,n){var r="double"===(n.quoteStyle||t)?'"':"'";return r+e+r}function B(e){return b.call(String(e),/"/g,""")}function $(e){return!("[object Array]"!==W(e)||N&&"object"==typeof e&&N in e)}function q(e){return!("[object RegExp]"!==W(e)||N&&"object"==typeof e&&N in e)}function U(e){if(P)return e&&"object"==typeof e&&e instanceof Symbol;if("symbol"==typeof e)return!0;if(!e||"object"!=typeof e||!C)return!1;try{return C.call(e),!0}catch(e){}return!1}e.exports=function e(t,n,r,o){var a=n||{};if(V(a,"quoteStyle")&&"single"!==a.quoteStyle&&"double"!==a.quoteStyle)throw new TypeError('option "quoteStyle" must be "single" or "double"');if(V(a,"maxStringLength")&&("number"==typeof a.maxStringLength?a.maxStringLength<0&&a.maxStringLength!==1/0:null!==a.maxStringLength))throw new TypeError('option "maxStringLength", if provided, must be a positive integer, Infinity, or `null`');var l=!V(a,"customInspect")||a.customInspect;if("boolean"!=typeof l&&"symbol"!==l)throw new TypeError("option \"customInspect\", if provided, must be `true`, `false`, or `'symbol'`");if(V(a,"indent")&&null!==a.indent&&"\t"!==a.indent&&!(parseInt(a.indent,10)===a.indent&&a.indent>0))throw new TypeError('option "indent" must be "\\t", an integer > 0, or `null`');if(V(a,"numericSeparator")&&"boolean"!=typeof a.numericSeparator)throw new TypeError('option "numericSeparator", if provided, must be `true` or `false`');var m=a.numericSeparator;if(void 0===t)return"undefined";if(null===t)return"null";if("boolean"==typeof t)return t?"true":"false";if("string"==typeof t)return K(t,a);if("number"==typeof t){if(0===t)return 1/0/t>0?"0":"-0";var w=String(t);return m?R(t,w):w}if("bigint"==typeof t){var x=String(t)+"n";return m?R(t,x):x}var O=void 0===a.depth?5:a.depth;if(void 0===r&&(r=0),r>=O&&O>0&&"object"==typeof t)return $(t)?"[Array]":"[Object]";var A=function(e,t){var n;if("\t"===e.indent)n="\t";else{if(!("number"==typeof e.indent&&e.indent>0))return null;n=_.call(Array(e.indent+1)," ")}return{base:n,prev:_.call(Array(t+1),n)}}(a,r);if(void 0===o)o=[];else if(J(o,t)>=0)return"[Circular]";function D(t,n,s){if(n&&(o=j.call(o)).push(n),s){var i={depth:a.depth};return V(a,"quoteStyle")&&(i.quoteStyle=a.quoteStyle),e(t,i,r+1,o)}return e(t,a,r+1,o)}if("function"==typeof t&&!q(t)){var z=function(e){if(e.name)return e.name;var t=y.call(g.call(e),/^function\s*([\w$]+)/);if(t)return t[1];return null}(t),H=Q(t,D);return"[Function"+(z?": "+z:" (anonymous)")+"]"+(H.length>0?" { "+_.call(H,", ")+" }":"")}if(U(t)){var ee=P?b.call(String(t),/^(Symbol\(.*\))_[^)]*$/,"$1"):C.call(t);return"object"!=typeof t||P?ee:G(ee)}if(function(e){if(!e||"object"!=typeof e)return!1;if("undefined"!=typeof HTMLElement&&e instanceof HTMLElement)return!0;return"string"==typeof e.nodeName&&"function"==typeof e.getAttribute}(t)){for(var te="<"+E.call(String(t.nodeName)),ne=t.attributes||[],re=0;re"}if($(t)){if(0===t.length)return"[]";var oe=Q(t,D);return A&&!function(e){for(var t=0;t=0)return!1;return!0}(oe)?"["+X(oe,A)+"]":"[ "+_.call(oe,", ")+" ]"}if(function(e){return!("[object Error]"!==W(e)||N&&"object"==typeof e&&N in e)}(t)){var se=Q(t,D);return"cause"in Error.prototype||!("cause"in t)||I.call(t,"cause")?0===se.length?"["+String(t)+"]":"{ ["+String(t)+"] "+_.call(se,", ")+" }":"{ ["+String(t)+"] "+_.call(S.call("[cause]: "+D(t.cause),se),", ")+" }"}if("object"==typeof t&&l){if(F&&"function"==typeof t[F]&&M)return M(t,{depth:O-r});if("symbol"!==l&&"function"==typeof t.inspect)return t.inspect()}if(function(e){if(!s||!e||"object"!=typeof e)return!1;try{s.call(e);try{c.call(e)}catch(e){return!0}return e instanceof Map}catch(e){}return!1}(t)){var ie=[];return i&&i.call(t,(function(e,n){ie.push(D(n,t,!0)+" => "+D(e,t))})),Y("Map",s.call(t),ie,A)}if(function(e){if(!c||!e||"object"!=typeof e)return!1;try{c.call(e);try{s.call(e)}catch(e){return!0}return e instanceof Set}catch(e){}return!1}(t)){var ae=[];return u&&u.call(t,(function(e){ae.push(D(e,t))})),Y("Set",c.call(t),ae,A)}if(function(e){if(!p||!e||"object"!=typeof e)return!1;try{p.call(e,p);try{h.call(e,h)}catch(e){return!0}return e instanceof WeakMap}catch(e){}return!1}(t))return Z("WeakMap");if(function(e){if(!h||!e||"object"!=typeof e)return!1;try{h.call(e,h);try{p.call(e,p)}catch(e){return!0}return e instanceof WeakSet}catch(e){}return!1}(t))return Z("WeakSet");if(function(e){if(!f||!e||"object"!=typeof e)return!1;try{return f.call(e),!0}catch(e){}return!1}(t))return Z("WeakRef");if(function(e){return!("[object Number]"!==W(e)||N&&"object"==typeof e&&N in e)}(t))return G(D(Number(t)));if(function(e){if(!e||"object"!=typeof e||!k)return!1;try{return k.call(e),!0}catch(e){}return!1}(t))return G(D(k.call(t)));if(function(e){return!("[object Boolean]"!==W(e)||N&&"object"==typeof e&&N in e)}(t))return G(d.call(t));if(function(e){return!("[object String]"!==W(e)||N&&"object"==typeof e&&N in e)}(t))return G(D(String(t)));if(!function(e){return!("[object Date]"!==W(e)||N&&"object"==typeof e&&N in e)}(t)&&!q(t)){var le=Q(t,D),ce=T?T(t)===Object.prototype:t instanceof Object||t.constructor===Object,ue=t instanceof Object?"":"null prototype",pe=!ce&&N&&Object(t)===t&&N in t?v.call(W(t),8,-1):ue?"Object":"",he=(ce||"function"!=typeof t.constructor?"":t.constructor.name?t.constructor.name+" ":"")+(pe||ue?"["+_.call(S.call([],pe||[],ue||[]),": ")+"] ":"");return 0===le.length?he+"{}":A?he+"{"+X(le,A)+"}":he+"{ "+_.call(le,", ")+" }"}return String(t)};var z=Object.prototype.hasOwnProperty||function(e){return e in this};function V(e,t){return z.call(e,t)}function W(e){return m.call(e)}function J(e,t){if(e.indexOf)return e.indexOf(t);for(var n=0,r=e.length;nt.maxStringLength){var n=e.length-t.maxStringLength,r="... "+n+" more character"+(n>1?"s":"");return K(v.call(e,0,t.maxStringLength),t)+r}return L(b.call(b.call(e,/(['\\])/g,"\\$1"),/[\x00-\x1f]/g,H),"single",t)}function H(e){var t=e.charCodeAt(0),n={8:"b",9:"t",10:"n",12:"f",13:"r"}[t];return n?"\\"+n:"\\x"+(t<16?"0":"")+w.call(t.toString(16))}function G(e){return"Object("+e+")"}function Z(e){return e+" { ? }"}function Y(e,t,n,r){return e+" ("+t+") {"+(r?X(n,r):_.call(n,", "))+"}"}function X(e,t){if(0===e.length)return"";var n="\n"+t.prev+t.base;return n+_.call(e,","+n)+"\n"+t.prev}function Q(e,t){var n=$(e),r=[];if(n){r.length=e.length;for(var o=0;o{var t,n,r=e.exports={};function o(){throw new Error("setTimeout has not been defined")}function s(){throw new Error("clearTimeout has not been defined")}function i(e){if(t===setTimeout)return setTimeout(e,0);if((t===o||!t)&&setTimeout)return t=setTimeout,setTimeout(e,0);try{return t(e,0)}catch(n){try{return t.call(null,e,0)}catch(n){return t.call(this,e,0)}}}!function(){try{t="function"==typeof setTimeout?setTimeout:o}catch(e){t=o}try{n="function"==typeof clearTimeout?clearTimeout:s}catch(e){n=s}}();var a,l=[],c=!1,u=-1;function p(){c&&a&&(c=!1,a.length?l=a.concat(l):u=-1,l.length&&h())}function h(){if(!c){var e=i(p);c=!0;for(var t=l.length;t;){for(a=l,l=[];++u1)for(var n=1;n{"use strict";var r=n(50414);function o(){}function s(){}s.resetWarningCache=o,e.exports=function(){function e(e,t,n,o,s,i){if(i!==r){var a=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw a.name="Invariant Violation",a}}function t(){return e}e.isRequired=e;var n={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:s,resetWarningCache:o};return n.PropTypes=n,n}},45697:(e,t,n)=>{e.exports=n(92703)()},50414:e=>{"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},55798:e=>{"use strict";var t=String.prototype.replace,n=/%20/g,r="RFC1738",o="RFC3986";e.exports={default:o,formatters:{RFC1738:function(e){return t.call(e,n,"+")},RFC3986:function(e){return String(e)}},RFC1738:r,RFC3986:o}},80129:(e,t,n)=>{"use strict";var r=n(58261),o=n(55235),s=n(55798);e.exports={formats:s,parse:o,stringify:r}},55235:(e,t,n)=>{"use strict";var r=n(12769),o=Object.prototype.hasOwnProperty,s=Array.isArray,i={allowDots:!1,allowPrototypes:!1,allowSparse:!1,arrayLimit:20,charset:"utf-8",charsetSentinel:!1,comma:!1,decoder:r.decode,delimiter:"&",depth:5,ignoreQueryPrefix:!1,interpretNumericEntities:!1,parameterLimit:1e3,parseArrays:!0,plainObjects:!1,strictNullHandling:!1},a=function(e){return e.replace(/&#(\d+);/g,(function(e,t){return String.fromCharCode(parseInt(t,10))}))},l=function(e,t){return e&&"string"==typeof e&&t.comma&&e.indexOf(",")>-1?e.split(","):e},c=function(e,t,n,r){if(e){var s=n.allowDots?e.replace(/\.([^.[]+)/g,"[$1]"):e,i=/(\[[^[\]]*])/g,a=n.depth>0&&/(\[[^[\]]*])/.exec(s),c=a?s.slice(0,a.index):s,u=[];if(c){if(!n.plainObjects&&o.call(Object.prototype,c)&&!n.allowPrototypes)return;u.push(c)}for(var p=0;n.depth>0&&null!==(a=i.exec(s))&&p=0;--s){var i,a=e[s];if("[]"===a&&n.parseArrays)i=[].concat(o);else{i=n.plainObjects?Object.create(null):{};var c="["===a.charAt(0)&&"]"===a.charAt(a.length-1)?a.slice(1,-1):a,u=parseInt(c,10);n.parseArrays||""!==c?!isNaN(u)&&a!==c&&String(u)===c&&u>=0&&n.parseArrays&&u<=n.arrayLimit?(i=[])[u]=o:"__proto__"!==c&&(i[c]=o):i={0:o}}o=i}return o}(u,t,n,r)}};e.exports=function(e,t){var n=function(e){if(!e)return i;if(null!==e.decoder&&void 0!==e.decoder&&"function"!=typeof e.decoder)throw new TypeError("Decoder has to be a function.");if(void 0!==e.charset&&"utf-8"!==e.charset&&"iso-8859-1"!==e.charset)throw new TypeError("The charset option must be either utf-8, iso-8859-1, or undefined");var t=void 0===e.charset?i.charset:e.charset;return{allowDots:void 0===e.allowDots?i.allowDots:!!e.allowDots,allowPrototypes:"boolean"==typeof e.allowPrototypes?e.allowPrototypes:i.allowPrototypes,allowSparse:"boolean"==typeof e.allowSparse?e.allowSparse:i.allowSparse,arrayLimit:"number"==typeof e.arrayLimit?e.arrayLimit:i.arrayLimit,charset:t,charsetSentinel:"boolean"==typeof e.charsetSentinel?e.charsetSentinel:i.charsetSentinel,comma:"boolean"==typeof e.comma?e.comma:i.comma,decoder:"function"==typeof e.decoder?e.decoder:i.decoder,delimiter:"string"==typeof e.delimiter||r.isRegExp(e.delimiter)?e.delimiter:i.delimiter,depth:"number"==typeof e.depth||!1===e.depth?+e.depth:i.depth,ignoreQueryPrefix:!0===e.ignoreQueryPrefix,interpretNumericEntities:"boolean"==typeof e.interpretNumericEntities?e.interpretNumericEntities:i.interpretNumericEntities,parameterLimit:"number"==typeof e.parameterLimit?e.parameterLimit:i.parameterLimit,parseArrays:!1!==e.parseArrays,plainObjects:"boolean"==typeof e.plainObjects?e.plainObjects:i.plainObjects,strictNullHandling:"boolean"==typeof e.strictNullHandling?e.strictNullHandling:i.strictNullHandling}}(t);if(""===e||null==e)return n.plainObjects?Object.create(null):{};for(var u="string"==typeof e?function(e,t){var n,c={},u=t.ignoreQueryPrefix?e.replace(/^\?/,""):e,p=t.parameterLimit===1/0?void 0:t.parameterLimit,h=u.split(t.delimiter,p),f=-1,d=t.charset;if(t.charsetSentinel)for(n=0;n-1&&(g=s(g)?[g]:g),o.call(c,m)?c[m]=r.combine(c[m],g):c[m]=g}return c}(e,n):e,p=n.plainObjects?Object.create(null):{},h=Object.keys(u),f=0;f{"use strict";var r=n(37478),o=n(12769),s=n(55798),i=Object.prototype.hasOwnProperty,a={brackets:function(e){return e+"[]"},comma:"comma",indices:function(e,t){return e+"["+t+"]"},repeat:function(e){return e}},l=Array.isArray,c=String.prototype.split,u=Array.prototype.push,p=function(e,t){u.apply(e,l(t)?t:[t])},h=Date.prototype.toISOString,f=s.default,d={addQueryPrefix:!1,allowDots:!1,charset:"utf-8",charsetSentinel:!1,delimiter:"&",encode:!0,encoder:o.encode,encodeValuesOnly:!1,format:f,formatter:s.formatters[f],indices:!1,serializeDate:function(e){return h.call(e)},skipNulls:!1,strictNullHandling:!1},m={},g=function e(t,n,s,i,a,u,h,f,g,y,v,b,w,E,x,S){for(var _,j=t,O=S,k=0,A=!1;void 0!==(O=O.get(m))&&!A;){var C=O.get(t);if(k+=1,void 0!==C){if(C===k)throw new RangeError("Cyclic object value");A=!0}void 0===O.get(m)&&(k=0)}if("function"==typeof f?j=f(n,j):j instanceof Date?j=v(j):"comma"===s&&l(j)&&(j=o.maybeMap(j,(function(e){return e instanceof Date?v(e):e}))),null===j){if(a)return h&&!E?h(n,d.encoder,x,"key",b):n;j=""}if("string"==typeof(_=j)||"number"==typeof _||"boolean"==typeof _||"symbol"==typeof _||"bigint"==typeof _||o.isBuffer(j)){if(h){var P=E?n:h(n,d.encoder,x,"key",b);if("comma"===s&&E){for(var N=c.call(String(j),","),I="",T=0;T0?j.join(",")||null:void 0}];else if(l(f))R=f;else{var D=Object.keys(j);R=g?D.sort(g):D}for(var F=i&&l(j)&&1===j.length?n+"[]":n,L=0;L0?E+w:""}},12769:(e,t,n)=>{"use strict";var r=n(55798),o=Object.prototype.hasOwnProperty,s=Array.isArray,i=function(){for(var e=[],t=0;t<256;++t)e.push("%"+((t<16?"0":"")+t.toString(16)).toUpperCase());return e}(),a=function(e,t){for(var n=t&&t.plainObjects?Object.create(null):{},r=0;r1;){var t=e.pop(),n=t.obj[t.prop];if(s(n)){for(var r=[],o=0;o=48&&u<=57||u>=65&&u<=90||u>=97&&u<=122||s===r.RFC1738&&(40===u||41===u)?l+=a.charAt(c):u<128?l+=i[u]:u<2048?l+=i[192|u>>6]+i[128|63&u]:u<55296||u>=57344?l+=i[224|u>>12]+i[128|u>>6&63]+i[128|63&u]:(c+=1,u=65536+((1023&u)<<10|1023&a.charCodeAt(c)),l+=i[240|u>>18]+i[128|u>>12&63]+i[128|u>>6&63]+i[128|63&u])}return l},isBuffer:function(e){return!(!e||"object"!=typeof e)&&!!(e.constructor&&e.constructor.isBuffer&&e.constructor.isBuffer(e))},isRegExp:function(e){return"[object RegExp]"===Object.prototype.toString.call(e)},maybeMap:function(e,t){if(s(e)){for(var n=[],r=0;r{"use strict";var n=Object.prototype.hasOwnProperty;function r(e){try{return decodeURIComponent(e.replace(/\+/g," "))}catch(e){return null}}function o(e){try{return encodeURIComponent(e)}catch(e){return null}}t.stringify=function(e,t){t=t||"";var r,s,i=[];for(s in"string"!=typeof t&&(t="?"),e)if(n.call(e,s)){if((r=e[s])||null!=r&&!isNaN(r)||(r=""),s=o(s),r=o(r),null===s||null===r)continue;i.push(s+"="+r)}return i.length?t+i.join("&"):""},t.parse=function(e){for(var t,n=/([^=?#&]+)=?([^&]*)/g,o={};t=n.exec(e);){var s=r(t[1]),i=r(t[2]);null===s||null===i||s in o||(o[s]=i)}return o}},14419:(e,t,n)=>{const r=n(60697),o=n(69450),s=r.types;e.exports=class e{constructor(e,t){if(this._setDefaults(e),e instanceof RegExp)this.ignoreCase=e.ignoreCase,this.multiline=e.multiline,e=e.source;else{if("string"!=typeof e)throw new Error("Expected a regexp or string");this.ignoreCase=t&&-1!==t.indexOf("i"),this.multiline=t&&-1!==t.indexOf("m")}this.tokens=r(e)}_setDefaults(t){this.max=null!=t.max?t.max:null!=e.prototype.max?e.prototype.max:100,this.defaultRange=t.defaultRange?t.defaultRange:this.defaultRange.clone(),t.randInt&&(this.randInt=t.randInt)}gen(){return this._gen(this.tokens,[])}_gen(e,t){var n,r,o,i,a;switch(e.type){case s.ROOT:case s.GROUP:if(e.followedBy||e.notFollowedBy)return"";for(e.remember&&void 0===e.groupNumber&&(e.groupNumber=t.push(null)-1),r="",i=0,a=(n=e.options?this._randSelect(e.options):e.stack).length;i{"use strict";var r=n(34155),o=65536,s=4294967295;var i=n(89509).Buffer,a=n.g.crypto||n.g.msCrypto;a&&a.getRandomValues?e.exports=function(e,t){if(e>s)throw new RangeError("requested too many random bytes");var n=i.allocUnsafe(e);if(e>0)if(e>o)for(var l=0;l{"use strict";function r(e){return r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},r(e)}Object.defineProperty(t,"__esModule",{value:!0}),t.CopyToClipboard=void 0;var o=a(n(67294)),s=a(n(20640)),i=["text","onCopy","options","children"];function a(e){return e&&e.__esModule?e:{default:e}}function l(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function c(e){for(var t=1;t=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function p(e,t){for(var n=0;n{"use strict";var r=n(74300).CopyToClipboard;r.CopyToClipboard=r,e.exports=r},53441:(e,t,n)=>{"use strict";function r(e){return r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},r(e)}Object.defineProperty(t,"__esModule",{value:!0}),t.DebounceInput=void 0;var o=a(n(67294)),s=a(n(91296)),i=["element","onChange","value","minLength","debounceTimeout","forceNotifyByEnter","forceNotifyOnBlur","onKeyDown","onBlur","inputRef"];function a(e){return e&&e.__esModule?e:{default:e}}function l(e,t){if(null==e)return{};var n,r,o=function(e,t){if(null==e)return{};var n,r,o={},s=Object.keys(e);for(r=0;r=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function c(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function u(e){for(var t=1;t=r?t.notify(e):n.length>o.length&&t.notify(u(u({},e),{},{target:u(u({},e.target),{},{value:""})}))}))})),g(d(t),"onKeyDown",(function(e){"Enter"===e.key&&t.forceNotify(e);var n=t.props.onKeyDown;n&&(e.persist(),n(e))})),g(d(t),"onBlur",(function(e){t.forceNotify(e);var n=t.props.onBlur;n&&(e.persist(),n(e))})),g(d(t),"createNotifier",(function(e){if(e<0)t.notify=function(){return null};else if(0===e)t.notify=t.doNotify;else{var n=(0,s.default)((function(e){t.isDebouncing=!1,t.doNotify(e)}),e);t.notify=function(e){t.isDebouncing=!0,n(e)},t.flush=function(){return n.flush()},t.cancel=function(){t.isDebouncing=!1,n.cancel()}}})),g(d(t),"doNotify",(function(){t.props.onChange.apply(void 0,arguments)})),g(d(t),"forceNotify",(function(e){var n=t.props.debounceTimeout;if(t.isDebouncing||!(n>0)){t.cancel&&t.cancel();var r=t.state.value,o=t.props.minLength;r.length>=o?t.doNotify(e):t.doNotify(u(u({},e),{},{target:u(u({},e.target),{},{value:r})}))}})),t.isDebouncing=!1,t.state={value:void 0===e.value||null===e.value?"":e.value};var n=t.props.debounceTimeout;return t.createNotifier(n),t}return t=c,(n=[{key:"componentDidUpdate",value:function(e){if(!this.isDebouncing){var t=this.props,n=t.value,r=t.debounceTimeout,o=e.debounceTimeout,s=e.value,i=this.state.value;void 0!==n&&s!==n&&i!==n&&this.setState({value:n}),r!==o&&this.createNotifier(r)}}},{key:"componentWillUnmount",value:function(){this.flush&&this.flush()}},{key:"render",value:function(){var e,t,n=this.props,r=n.element,s=(n.onChange,n.value,n.minLength,n.debounceTimeout,n.forceNotifyByEnter),a=n.forceNotifyOnBlur,c=n.onKeyDown,p=n.onBlur,h=n.inputRef,f=l(n,i),d=this.state.value;e=s?{onKeyDown:this.onKeyDown}:c?{onKeyDown:c}:{},t=a?{onBlur:this.onBlur}:p?{onBlur:p}:{};var m=h?{ref:h}:{};return o.default.createElement(r,u(u(u(u({},f),{},{onChange:this.onChange,value:d},e),t),m))}}])&&p(t.prototype,n),r&&p(t,r),Object.defineProperty(t,"prototype",{writable:!1}),c}(o.default.PureComponent);t.DebounceInput=y,g(y,"defaultProps",{element:"input",type:"text",onKeyDown:void 0,onBlur:void 0,value:void 0,minLength:0,debounceTimeout:100,forceNotifyByEnter:!0,forceNotifyOnBlur:!0,inputRef:void 0})},775:(e,t,n)=>{"use strict";var r=n(53441).DebounceInput;r.DebounceInput=r,e.exports=r},64448:(e,t,n)=>{"use strict";var r=n(67294),o=n(27418),s=n(63840);function i(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n
\n \n )\n}\n\nAccordion.propTypes = {\n expanded: PropTypes.bool,\n children: PropTypes.node.isRequired,\n onChange: PropTypes.func.isRequired,\n}\n\nAccordion.defaultProps = {\n expanded: false,\n}\n\nexport default Accordion\n","/**\n * @prettier\n */\nimport React, { useCallback } from \"react\"\nimport PropTypes from \"prop-types\"\n\nconst ExpandDeepButton = ({ expanded, onClick }) => {\n const handleExpansion = useCallback(\n (event) => {\n onClick(event, !expanded)\n },\n [expanded, onClick]\n )\n\n return (\n \n {expanded ? \"Collapse all\" : \"Expand all\"}\n \n )\n}\n\nExpandDeepButton.propTypes = {\n expanded: PropTypes.bool.isRequired,\n onClick: PropTypes.func.isRequired,\n}\n\nexport default ExpandDeepButton\n","/**\n * @prettier\n */\nimport React, { forwardRef, useState, useCallback, useEffect } from \"react\"\nimport PropTypes from \"prop-types\"\nimport classNames from \"classnames\"\n\nimport * as propTypes from \"../../prop-types\"\nimport {\n useComponent,\n useLevel,\n useFn,\n useIsEmbedded,\n useIsExpanded,\n useIsExpandedDeeply,\n useIsCircular,\n useRenderedSchemas,\n} from \"../../hooks\"\nimport {\n JSONSchemaLevelContext,\n JSONSchemaDeepExpansionContext,\n JSONSchemaCyclesContext,\n} from \"../../context\"\n\nconst JSONSchema = forwardRef(\n ({ schema, name, dependentRequired, onExpand }, ref) => {\n const fn = useFn()\n const isExpanded = useIsExpanded()\n const isExpandedDeeply = useIsExpandedDeeply()\n const [expanded, setExpanded] = useState(isExpanded || isExpandedDeeply)\n const [expandedDeeply, setExpandedDeeply] = useState(isExpandedDeeply)\n const [level, nextLevel] = useLevel()\n const isEmbedded = useIsEmbedded()\n const isExpandable = fn.isExpandable(schema) || dependentRequired.length > 0\n const isCircular = useIsCircular(schema)\n const renderedSchemas = useRenderedSchemas(schema)\n const constraints = fn.stringifyConstraints(schema)\n const Accordion = useComponent(\"Accordion\")\n const Keyword$schema = useComponent(\"Keyword$schema\")\n const Keyword$vocabulary = useComponent(\"Keyword$vocabulary\")\n const Keyword$id = useComponent(\"Keyword$id\")\n const Keyword$anchor = useComponent(\"Keyword$anchor\")\n const Keyword$dynamicAnchor = useComponent(\"Keyword$dynamicAnchor\")\n const Keyword$ref = useComponent(\"Keyword$ref\")\n const Keyword$dynamicRef = useComponent(\"Keyword$dynamicRef\")\n const Keyword$defs = useComponent(\"Keyword$defs\")\n const Keyword$comment = useComponent(\"Keyword$comment\")\n const KeywordAllOf = useComponent(\"KeywordAllOf\")\n const KeywordAnyOf = useComponent(\"KeywordAnyOf\")\n const KeywordOneOf = useComponent(\"KeywordOneOf\")\n const KeywordNot = useComponent(\"KeywordNot\")\n const KeywordIf = useComponent(\"KeywordIf\")\n const KeywordThen = useComponent(\"KeywordThen\")\n const KeywordElse = useComponent(\"KeywordElse\")\n const KeywordDependentSchemas = useComponent(\"KeywordDependentSchemas\")\n const KeywordPrefixItems = useComponent(\"KeywordPrefixItems\")\n const KeywordItems = useComponent(\"KeywordItems\")\n const KeywordContains = useComponent(\"KeywordContains\")\n const KeywordProperties = useComponent(\"KeywordProperties\")\n const KeywordPatternProperties = useComponent(\"KeywordPatternProperties\")\n const KeywordAdditionalProperties = useComponent(\n \"KeywordAdditionalProperties\"\n )\n const KeywordPropertyNames = useComponent(\"KeywordPropertyNames\")\n const KeywordUnevaluatedItems = useComponent(\"KeywordUnevaluatedItems\")\n const KeywordUnevaluatedProperties = useComponent(\n \"KeywordUnevaluatedProperties\"\n )\n const KeywordType = useComponent(\"KeywordType\")\n const KeywordEnum = useComponent(\"KeywordEnum\")\n const KeywordConst = useComponent(\"KeywordConst\")\n const KeywordConstraint = useComponent(\"KeywordConstraint\")\n const KeywordDependentRequired = useComponent(\"KeywordDependentRequired\")\n const KeywordContentSchema = useComponent(\"KeywordContentSchema\")\n const KeywordTitle = useComponent(\"KeywordTitle\")\n const KeywordDescription = useComponent(\"KeywordDescription\")\n const KeywordDefault = useComponent(\"KeywordDefault\")\n const KeywordDeprecated = useComponent(\"KeywordDeprecated\")\n const KeywordReadOnly = useComponent(\"KeywordReadOnly\")\n const KeywordWriteOnly = useComponent(\"KeywordWriteOnly\")\n const ExpandDeepButton = useComponent(\"ExpandDeepButton\")\n\n /**\n * Effects handlers.\n */\n useEffect(() => {\n setExpandedDeeply(isExpandedDeeply)\n }, [isExpandedDeeply])\n\n useEffect(() => {\n setExpandedDeeply(expandedDeeply)\n }, [expandedDeeply])\n\n /**\n * Event handlers.\n */\n const handleExpansion = useCallback(\n (e, expandedNew) => {\n setExpanded(expandedNew)\n !expandedNew && setExpandedDeeply(false)\n onExpand(e, expandedNew, false)\n },\n [onExpand]\n )\n const handleExpansionDeep = useCallback(\n (e, expandedDeepNew) => {\n setExpanded(expandedDeepNew)\n setExpandedDeeply(expandedDeepNew)\n onExpand(e, expandedDeepNew, true)\n },\n [onExpand]\n )\n\n return (\n \n \n \n \n
\n {isExpandable && !isCircular ? (\n <>\n \n \n \n \n \n ) : (\n \n )}\n \n \n \n \n {constraints.length > 0 &&\n constraints.map((constraint) => (\n \n ))}\n
\n \n {expanded && (\n <>\n \n {!isCircular && isExpandable && (\n <>\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n )}\n \n \n \n \n \n \n \n \n \n \n {!isCircular && isExpandable && (\n \n )}\n \n \n \n )}\n \n \n
\n
\n
\n )\n }\n)\n\nJSONSchema.propTypes = {\n name: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),\n schema: propTypes.schema.isRequired,\n dependentRequired: PropTypes.arrayOf(PropTypes.string),\n onExpand: PropTypes.func,\n}\n\nJSONSchema.defaultProps = {\n name: \"\",\n dependentRequired: [],\n onExpand: () => {},\n}\n\nexport default JSONSchema\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nconst ChevronRight = () => (\n \n \n \n)\n\nexport default ChevronRight\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../prop-types\"\n\nconst $anchor = ({ schema }) => {\n if (!schema?.$anchor) return null\n\n return (\n
\n \n $anchor\n \n \n {schema.$anchor}\n \n
\n )\n}\n\n$anchor.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default $anchor\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../prop-types\"\n\nconst $comment = ({ schema }) => {\n if (!schema?.$comment) return null\n\n return (\n
\n \n $comment\n \n \n {schema.$comment}\n \n
\n )\n}\n\n$comment.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default $comment\n","/**\n * @prettier\n */\nimport React, { useCallback, useState } from \"react\"\nimport classNames from \"classnames\"\n\nimport { schema } from \"../../prop-types\"\nimport { useComponent, useIsExpandedDeeply } from \"../../hooks\"\nimport { JSONSchemaDeepExpansionContext } from \"../../context\"\n\nconst $defs = ({ schema }) => {\n const $defs = schema?.$defs || {}\n const isExpandedDeeply = useIsExpandedDeeply()\n const [expanded, setExpanded] = useState(isExpandedDeeply)\n const [expandedDeeply, setExpandedDeeply] = useState(false)\n const Accordion = useComponent(\"Accordion\")\n const ExpandDeepButton = useComponent(\"ExpandDeepButton\")\n const JSONSchema = useComponent(\"JSONSchema\")\n\n /**\n * Event handlers.\n */\n const handleExpansion = useCallback(() => {\n setExpanded((prev) => !prev)\n }, [])\n const handleExpansionDeep = useCallback((e, expandedDeepNew) => {\n setExpanded(expandedDeepNew)\n setExpandedDeeply(expandedDeepNew)\n }, [])\n\n /**\n * Rendering.\n */\n if (Object.keys($defs).length === 0) {\n return null\n }\n\n return (\n \n
\n \n \n $defs\n \n \n \n \n object\n \n \n {expanded && (\n <>\n {Object.entries($defs).map(([schemaName, schema]) => (\n
  • \n \n
  • \n ))}\n \n )}\n \n
    \n
    \n )\n}\n\n$defs.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default $defs\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../prop-types\"\n\nconst $dynamicAnchor = ({ schema }) => {\n if (!schema?.$dynamicAnchor) return null\n\n return (\n
    \n \n $dynamicAnchor\n \n \n {schema.$dynamicAnchor}\n \n
    \n )\n}\n\n$dynamicAnchor.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default $dynamicAnchor\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../prop-types\"\n\nconst $dynamicRef = ({ schema }) => {\n if (!schema?.$dynamicRef) return null\n\n return (\n
    \n \n $dynamicRef\n \n \n {schema.$dynamicRef}\n \n
    \n )\n}\n\n$dynamicRef.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default $dynamicRef\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../prop-types\"\n\nconst $id = ({ schema }) => {\n if (!schema?.$id) return null\n\n return (\n
    \n \n $id\n \n \n {schema.$id}\n \n
    \n )\n}\n\n$id.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default $id\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../prop-types\"\n\nconst $ref = ({ schema }) => {\n if (!schema?.$ref) return null\n\n return (\n
    \n \n $ref\n \n \n {schema.$ref}\n \n
    \n )\n}\n\n$ref.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default $ref\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../prop-types\"\n\nconst $schema = ({ schema }) => {\n if (!schema?.$schema) return null\n\n return (\n
    \n \n $schema\n \n \n {schema.$schema}\n \n
    \n )\n}\n\n$schema.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default $schema\n","/**\n * @prettier\n */\nimport React, { useCallback, useState } from \"react\"\nimport classNames from \"classnames\"\n\nimport { schema } from \"../../../prop-types\"\nimport { useComponent, useIsExpandedDeeply } from \"../../../hooks\"\n\nconst $vocabulary = ({ schema }) => {\n const isExpandedDeeply = useIsExpandedDeeply()\n const [expanded, setExpanded] = useState(isExpandedDeeply)\n const Accordion = useComponent(\"Accordion\")\n\n const handleExpansion = useCallback(() => {\n setExpanded((prev) => !prev)\n }, [])\n\n /**\n * Rendering.\n */\n if (!schema?.$vocabulary) return null\n if (typeof schema.$vocabulary !== \"object\") return null\n\n return (\n
    \n \n \n $vocabulary\n \n \n \n object\n \n
      \n {expanded &&\n Object.entries(schema.$vocabulary).map(([uri, enabled]) => (\n \n \n {uri}\n \n \n ))}\n
    \n
    \n )\n}\n\n$vocabulary.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default $vocabulary\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../prop-types\"\nimport { useFn, useComponent } from \"../../hooks\"\n\nconst AdditionalProperties = ({ schema }) => {\n const fn = useFn()\n const { additionalProperties } = schema\n const JSONSchema = useComponent(\"JSONSchema\")\n\n if (!fn.hasKeyword(schema, \"additionalProperties\")) return null\n\n /**\n * Rendering.\n */\n const name = (\n \n Additional properties\n \n )\n\n return (\n
    \n {additionalProperties === true ? (\n <>\n {name}\n \n allowed\n \n \n ) : additionalProperties === false ? (\n <>\n {name}\n \n forbidden\n \n \n ) : (\n \n )}\n
    \n )\n}\n\nAdditionalProperties.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default AdditionalProperties\n","/**\n * @prettier\n */\nimport React, { useCallback, useState } from \"react\"\nimport classNames from \"classnames\"\n\nimport { schema } from \"../../prop-types\"\nimport { useFn, useComponent, useIsExpandedDeeply } from \"../../hooks\"\nimport { JSONSchemaDeepExpansionContext } from \"../../context\"\n\nconst AllOf = ({ schema }) => {\n const allOf = schema?.allOf || []\n const fn = useFn()\n const isExpandedDeeply = useIsExpandedDeeply()\n const [expanded, setExpanded] = useState(isExpandedDeeply)\n const [expandedDeeply, setExpandedDeeply] = useState(false)\n const Accordion = useComponent(\"Accordion\")\n const ExpandDeepButton = useComponent(\"ExpandDeepButton\")\n const JSONSchema = useComponent(\"JSONSchema\")\n const KeywordType = useComponent(\"KeywordType\")\n\n /**\n * Event handlers.\n */\n const handleExpansion = useCallback(() => {\n setExpanded((prev) => !prev)\n }, [])\n const handleExpansionDeep = useCallback((e, expandedDeepNew) => {\n setExpanded(expandedDeepNew)\n setExpandedDeeply(expandedDeepNew)\n }, [])\n\n /**\n * Rendering.\n */\n if (!Array.isArray(allOf) || allOf.length === 0) {\n return null\n }\n\n return (\n \n
    \n \n \n All of\n \n \n \n \n \n {expanded && (\n <>\n {allOf.map((schema, index) => (\n
  • \n \n
  • \n ))}\n \n )}\n \n
    \n
    \n )\n}\n\nAllOf.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default AllOf\n","/**\n * @prettier\n */\nimport React, { useCallback, useState } from \"react\"\nimport classNames from \"classnames\"\n\nimport { schema } from \"../../prop-types\"\nimport { useFn, useComponent, useIsExpandedDeeply } from \"../../hooks\"\nimport { JSONSchemaDeepExpansionContext } from \"../../context\"\n\nconst AnyOf = ({ schema }) => {\n const anyOf = schema?.anyOf || []\n const fn = useFn()\n const isExpandedDeeply = useIsExpandedDeeply()\n const [expanded, setExpanded] = useState(isExpandedDeeply)\n const [expandedDeeply, setExpandedDeeply] = useState(false)\n const Accordion = useComponent(\"Accordion\")\n const ExpandDeepButton = useComponent(\"ExpandDeepButton\")\n const JSONSchema = useComponent(\"JSONSchema\")\n const KeywordType = useComponent(\"KeywordType\")\n\n /**\n * Event handlers.\n */\n const handleExpansion = useCallback(() => {\n setExpanded((prev) => !prev)\n }, [])\n const handleExpansionDeep = useCallback((e, expandedDeepNew) => {\n setExpanded(expandedDeepNew)\n setExpandedDeeply(expandedDeepNew)\n }, [])\n\n /**\n * Rendering.\n */\n if (!Array.isArray(anyOf) || anyOf.length === 0) {\n return null\n }\n\n return (\n \n
    \n \n \n Any of\n \n \n \n \n \n {expanded && (\n <>\n {anyOf.map((schema, index) => (\n
  • \n \n
  • \n ))}\n \n )}\n \n
    \n
    \n )\n}\n\nAnyOf.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default AnyOf\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../prop-types\"\nimport { useFn } from \"../../hooks\"\n\nconst Const = ({ schema }) => {\n const fn = useFn()\n\n if (!fn.hasKeyword(schema, \"const\")) return null\n\n return (\n
    \n \n Const\n \n \n {fn.stringify(schema.const)}\n \n
    \n )\n}\n\nConst.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default Const\n","/**\n * @prettier\n */\nimport React from \"react\"\nimport PropTypes from \"prop-types\"\n\n/**\n * This component represents various constraint keywords\n * from JSON Schema 2020-12 validation vocabulary.\n */\nconst Constraint = ({ constraint }) => (\n \n {constraint.value}\n \n)\n\nConstraint.propTypes = {\n constraint: PropTypes.shape({\n scope: PropTypes.oneOf([\"number\", \"string\", \"array\", \"object\"]).isRequired,\n value: PropTypes.string.isRequired,\n }).isRequired,\n}\n\nexport default React.memo(Constraint)\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../prop-types\"\nimport { useFn, useComponent } from \"../../hooks\"\n\nconst Contains = ({ schema }) => {\n const fn = useFn()\n const JSONSchema = useComponent(\"JSONSchema\")\n\n /**\n * Rendering.\n */\n if (!fn.hasKeyword(schema, \"contains\")) return null\n\n const name = (\n \n Contains\n \n )\n\n return (\n
    \n \n
    \n )\n}\n\nContains.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default Contains\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../prop-types\"\nimport { useFn, useComponent } from \"../../hooks\"\n\nconst ContentSchema = ({ schema }) => {\n const fn = useFn()\n const JSONSchema = useComponent(\"JSONSchema\")\n\n /**\n * Rendering.\n */\n if (!fn.hasKeyword(schema, \"contentSchema\")) return null\n\n const name = (\n \n Content schema\n \n )\n\n return (\n
    \n \n
    \n )\n}\n\nContentSchema.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default ContentSchema\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../prop-types\"\nimport { useFn } from \"../../hooks\"\n\nconst Default = ({ schema }) => {\n const fn = useFn()\n\n if (!fn.hasKeyword(schema, \"default\")) return null\n\n return (\n
    \n \n Default\n \n \n {fn.stringify(schema.default)}\n \n
    \n )\n}\n\nDefault.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default Default\n","/**\n * @prettier\n */\nimport React from \"react\"\nimport PropTypes from \"prop-types\"\n\nimport * as propTypes from \"../../../prop-types\"\n\nconst DependentRequired = ({ dependentRequired }) => {\n if (dependentRequired.length === 0) return null\n\n return (\n
    \n \n Required when defined\n \n
      \n {dependentRequired.map((propertyName) => (\n
    • \n \n {propertyName}\n \n
    • \n ))}\n
    \n
    \n )\n}\n\nDependentRequired.propTypes = {\n schema: propTypes.schema.isRequired,\n dependentRequired: PropTypes.arrayOf(PropTypes.string).isRequired,\n}\n\nexport default DependentRequired\n","/**\n * @prettier\n */\nimport React, { useCallback, useState } from \"react\"\nimport classNames from \"classnames\"\n\nimport { schema } from \"../../prop-types\"\nimport { useComponent, useIsExpandedDeeply } from \"../../hooks\"\nimport { JSONSchemaDeepExpansionContext } from \"../../context\"\n\nconst DependentSchemas = ({ schema }) => {\n const dependentSchemas = schema?.dependentSchemas || []\n const isExpandedDeeply = useIsExpandedDeeply()\n const [expanded, setExpanded] = useState(isExpandedDeeply)\n const [expandedDeeply, setExpandedDeeply] = useState(false)\n const Accordion = useComponent(\"Accordion\")\n const ExpandDeepButton = useComponent(\"ExpandDeepButton\")\n const JSONSchema = useComponent(\"JSONSchema\")\n\n /**\n * Event handlers.\n */\n const handleExpansion = useCallback(() => {\n setExpanded((prev) => !prev)\n }, [])\n const handleExpansionDeep = useCallback((e, expandedDeepNew) => {\n setExpanded(expandedDeepNew)\n setExpandedDeeply(expandedDeepNew)\n }, [])\n\n /**\n * Rendering.\n */\n if (typeof dependentSchemas !== \"object\") return null\n if (Object.keys(dependentSchemas).length === 0) return null\n\n return (\n \n
    \n \n \n Dependent schemas\n \n \n \n \n object\n \n \n {expanded && (\n <>\n {Object.entries(dependentSchemas).map(([schemaName, schema]) => (\n
  • \n \n
  • \n ))}\n \n )}\n \n
    \n
    \n )\n}\n\nDependentSchemas.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default DependentSchemas\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../prop-types\"\n\nconst Deprecated = ({ schema }) => {\n if (schema?.deprecated !== true) return null\n\n return (\n \n deprecated\n \n )\n}\n\nDeprecated.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default Deprecated\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../../prop-types\"\n\nconst Description = ({ schema }) => {\n if (!schema?.description) return null\n\n return (\n
    \n
    \n {schema.description}\n
    \n
    \n )\n}\n\nDescription.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default Description\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../prop-types\"\nimport { useFn, useComponent } from \"../../hooks\"\n\nconst Else = ({ schema }) => {\n const fn = useFn()\n const JSONSchema = useComponent(\"JSONSchema\")\n\n /**\n * Rendering.\n */\n if (!fn.hasKeyword(schema, \"else\")) return null\n\n const name = (\n \n Else\n \n )\n\n return (\n
    \n \n
    \n )\n}\n\nElse.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default Else\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../../prop-types\"\nimport { useFn } from \"../../../hooks\"\n\nconst Enum = ({ schema }) => {\n const fn = useFn()\n\n if (!Array.isArray(schema?.enum)) return null\n\n return (\n
    \n \n Allowed values\n \n
      \n {schema.enum.map((element) => {\n const strigifiedElement = fn.stringify(element)\n\n return (\n
    • \n \n {strigifiedElement}\n \n
    • \n )\n })}\n
    \n
    \n )\n}\n\nEnum.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default Enum\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../prop-types\"\nimport { useFn, useComponent } from \"../../hooks\"\n\nconst If = ({ schema }) => {\n const fn = useFn()\n const JSONSchema = useComponent(\"JSONSchema\")\n\n /**\n * Rendering.\n */\n if (!fn.hasKeyword(schema, \"if\")) return null\n\n const name = (\n \n If\n \n )\n\n return (\n
    \n \n
    \n )\n}\n\nIf.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default If\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../prop-types\"\nimport { useFn, useComponent } from \"../../hooks\"\n\nconst Items = ({ schema }) => {\n const fn = useFn()\n const JSONSchema = useComponent(\"JSONSchema\")\n\n /**\n * Rendering.\n */\n if (!fn.hasKeyword(schema, \"items\")) return null\n\n const name = (\n \n Items\n \n )\n\n return (\n
    \n \n
    \n )\n}\n\nItems.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default Items\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../prop-types\"\nimport { useFn, useComponent } from \"../../hooks\"\n\nconst Not = ({ schema }) => {\n const fn = useFn()\n const JSONSchema = useComponent(\"JSONSchema\")\n\n /**\n * Rendering.\n */\n if (!fn.hasKeyword(schema, \"not\")) return null\n\n const name = (\n \n Not\n \n )\n\n return (\n
    \n \n
    \n )\n}\n\nNot.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default Not\n","/**\n * @prettier\n */\nimport React, { useCallback, useState } from \"react\"\nimport classNames from \"classnames\"\n\nimport { schema } from \"../../prop-types\"\nimport { useFn, useComponent, useIsExpandedDeeply } from \"../../hooks\"\nimport { JSONSchemaDeepExpansionContext } from \"../../context\"\n\nconst OneOf = ({ schema }) => {\n const oneOf = schema?.oneOf || []\n const fn = useFn()\n const isExpandedDeeply = useIsExpandedDeeply()\n const [expanded, setExpanded] = useState(isExpandedDeeply)\n const [expandedDeeply, setExpandedDeeply] = useState(false)\n const Accordion = useComponent(\"Accordion\")\n const ExpandDeepButton = useComponent(\"ExpandDeepButton\")\n const JSONSchema = useComponent(\"JSONSchema\")\n const KeywordType = useComponent(\"KeywordType\")\n\n /**\n * Event handlers.\n */\n const handleExpansion = useCallback(() => {\n setExpanded((prev) => !prev)\n }, [])\n const handleExpansionDeep = useCallback((e, expandedDeepNew) => {\n setExpanded(expandedDeepNew)\n setExpandedDeeply(expandedDeepNew)\n }, [])\n\n /**\n * Rendering.\n */\n if (!Array.isArray(oneOf) || oneOf.length === 0) {\n return null\n }\n\n return (\n \n
    \n \n \n One of\n \n \n \n \n \n {expanded && (\n <>\n {oneOf.map((schema, index) => (\n
  • \n \n
  • \n ))}\n \n )}\n \n
    \n
    \n )\n}\n\nOneOf.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default OneOf\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../../prop-types\"\nimport { useComponent } from \"../../../hooks\"\n\nconst PatternProperties = ({ schema }) => {\n const patternProperties = schema?.patternProperties || {}\n const JSONSchema = useComponent(\"JSONSchema\")\n\n /**\n * Rendering.\n */\n if (Object.keys(patternProperties).length === 0) {\n return null\n }\n\n return (\n
    \n
      \n {Object.entries(patternProperties).map(([propertyName, schema]) => (\n
    • \n \n
    • \n ))}\n
    \n
    \n )\n}\n\nPatternProperties.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default PatternProperties\n","/**\n * @prettier\n */\nimport React, { useCallback, useState } from \"react\"\nimport classNames from \"classnames\"\n\nimport { schema } from \"../../prop-types\"\nimport { useFn, useComponent, useIsExpandedDeeply } from \"../../hooks\"\nimport { JSONSchemaDeepExpansionContext } from \"../../context\"\n\nconst PrefixItems = ({ schema }) => {\n const prefixItems = schema?.prefixItems || []\n const fn = useFn()\n const isExpandedDeeply = useIsExpandedDeeply()\n const [expanded, setExpanded] = useState(isExpandedDeeply)\n const [expandedDeeply, setExpandedDeeply] = useState(false)\n const Accordion = useComponent(\"Accordion\")\n const ExpandDeepButton = useComponent(\"ExpandDeepButton\")\n const JSONSchema = useComponent(\"JSONSchema\")\n const KeywordType = useComponent(\"KeywordType\")\n\n /**\n * Event handlers.\n */\n const handleExpansion = useCallback(() => {\n setExpanded((prev) => !prev)\n }, [])\n const handleExpansionDeep = useCallback((e, expandedDeepNew) => {\n setExpanded(expandedDeepNew)\n setExpandedDeeply(expandedDeepNew)\n }, [])\n\n /**\n * Rendering.\n */\n if (!Array.isArray(prefixItems) || prefixItems.length === 0) {\n return null\n }\n\n return (\n \n
    \n \n \n Prefix items\n \n \n \n \n \n {expanded && (\n <>\n {prefixItems.map((schema, index) => (\n
  • \n \n
  • \n ))}\n \n )}\n \n
    \n
    \n )\n}\n\nPrefixItems.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default PrefixItems\n","/**\n * @prettier\n */\nimport React from \"react\"\nimport classNames from \"classnames\"\n\nimport { schema } from \"../../../prop-types\"\nimport { useFn, useComponent } from \"../../../hooks\"\n\nconst Properties = ({ schema }) => {\n const fn = useFn()\n const properties = schema?.properties || {}\n const required = Array.isArray(schema?.required) ? schema.required : []\n const JSONSchema = useComponent(\"JSONSchema\")\n\n /**\n * Rendering.\n */\n if (Object.keys(properties).length === 0) {\n return null\n }\n\n return (\n
    \n
      \n {Object.entries(properties).map(([propertyName, propertySchema]) => {\n const isRequired = required.includes(propertyName)\n const dependentRequired = fn.getDependentRequired(\n propertyName,\n schema\n )\n\n return (\n \n \n \n )\n })}\n
    \n
    \n )\n}\n\nProperties.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default Properties\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../prop-types\"\nimport { useFn, useComponent } from \"../../hooks\"\n\nconst PropertyNames = ({ schema }) => {\n const fn = useFn()\n const { propertyNames } = schema\n const JSONSchema = useComponent(\"JSONSchema\")\n const name = (\n \n Property names\n \n )\n\n /**\n * Rendering.\n */\n if (!fn.hasKeyword(schema, \"propertyNames\")) return null\n\n return (\n
    \n \n
    \n )\n}\n\nPropertyNames.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default PropertyNames\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../prop-types\"\n\nconst ReadOnly = ({ schema }) => {\n if (schema?.readOnly !== true) return null\n\n return (\n \n read-only\n \n )\n}\n\nReadOnly.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default ReadOnly\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../prop-types\"\nimport { useFn, useComponent } from \"../../hooks\"\n\nconst Then = ({ schema }) => {\n const fn = useFn()\n const JSONSchema = useComponent(\"JSONSchema\")\n\n /**\n * Rendering.\n */\n if (!fn.hasKeyword(schema, \"then\")) return null\n\n const name = (\n \n Then\n \n )\n\n return (\n
    \n \n
    \n )\n}\n\nThen.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default Then\n","/**\n * @prettier\n */\nimport React from \"react\"\nimport PropTypes from \"prop-types\"\n\nimport { schema } from \"../../../prop-types\"\nimport { useFn } from \"../../../hooks\"\n\nconst Title = ({ title, schema }) => {\n const fn = useFn()\n const renderedTitle = title || fn.getTitle(schema)\n\n if (!renderedTitle) return null\n\n return (\n
    \n {title || fn.getTitle(schema)}\n
    \n )\n}\n\nTitle.propTypes = {\n title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),\n schema: schema.isRequired,\n}\n\nTitle.defaultProps = {\n title: \"\",\n}\n\nexport default Title\n","/**\n * @prettier\n */\nimport React from \"react\"\nimport PropTypes from \"prop-types\"\n\nimport { schema } from \"../../prop-types\"\nimport { useFn } from \"../../hooks\"\n\nconst Type = ({ schema, isCircular }) => {\n const fn = useFn()\n const type = fn.getType(schema)\n const circularSuffix = isCircular ? \" [circular]\" : \"\"\n\n return (\n \n {`${type}${circularSuffix}`}\n \n )\n}\n\nType.propTypes = {\n schema: schema.isRequired,\n isCircular: PropTypes.bool,\n}\n\nType.defaultProps = {\n isCircular: false,\n}\n\nexport default Type\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../prop-types\"\nimport { useFn, useComponent } from \"../../hooks\"\n\nconst UnevaluatedItems = ({ schema }) => {\n const fn = useFn()\n const { unevaluatedItems } = schema\n const JSONSchema = useComponent(\"JSONSchema\")\n\n /**\n * Rendering.\n */\n if (!fn.hasKeyword(schema, \"unevaluatedItems\")) return null\n\n const name = (\n \n Unevaluated items\n \n )\n\n return (\n
    \n \n
    \n )\n}\n\nUnevaluatedItems.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default UnevaluatedItems\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../prop-types\"\nimport { useFn, useComponent } from \"../../hooks\"\n\nconst UnevaluatedProperties = ({ schema }) => {\n const fn = useFn()\n const { unevaluatedProperties } = schema\n const JSONSchema = useComponent(\"JSONSchema\")\n\n /**\n * Rendering.\n */\n if (!fn.hasKeyword(schema, \"unevaluatedProperties\")) return null\n\n const name = (\n \n Unevaluated properties\n \n )\n\n return (\n
    \n \n
    \n )\n}\n\nUnevaluatedProperties.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default UnevaluatedProperties\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { schema } from \"../../prop-types\"\n\nconst WriteOnly = ({ schema }) => {\n if (schema?.writeOnly !== true) return null\n\n return (\n \n write-only\n \n )\n}\n\nWriteOnly.propTypes = {\n schema: schema.isRequired,\n}\n\nexport default WriteOnly\n","/**\n * @prettier\n */\nimport { createContext } from \"react\"\n\nexport const JSONSchemaContext = createContext(null)\nJSONSchemaContext.displayName = \"JSONSchemaContext\"\n\nexport const JSONSchemaLevelContext = createContext(0)\nJSONSchemaLevelContext.displayName = \"JSONSchemaLevelContext\"\n\nexport const JSONSchemaDeepExpansionContext = createContext(false)\nJSONSchemaDeepExpansionContext.displayName = \"JSONSchemaDeepExpansionContext\"\n\nexport const JSONSchemaCyclesContext = createContext(new Set())\n","/**\n * @prettier\n */\nimport { useFn } from \"./hooks\"\n\nexport const upperFirst = (value) => {\n if (typeof value === \"string\") {\n return `${value.charAt(0).toUpperCase()}${value.slice(1)}`\n }\n return value\n}\n\nexport const getTitle = (schema) => {\n const fn = useFn()\n\n if (schema?.title) return fn.upperFirst(schema.title)\n if (schema?.$anchor) return fn.upperFirst(schema.$anchor)\n if (schema?.$id) return schema.$id\n\n return \"\"\n}\n\nexport const getType = (schema, processedSchemas = new WeakSet()) => {\n const fn = useFn()\n\n if (schema == null) {\n return \"any\"\n }\n\n if (fn.isBooleanJSONSchema(schema)) {\n return schema ? \"any\" : \"never\"\n }\n\n if (typeof schema !== \"object\") {\n return \"any\"\n }\n\n if (processedSchemas.has(schema)) {\n return \"any\" // detect a cycle\n }\n processedSchemas.add(schema)\n\n const { type, prefixItems, items } = schema\n\n const getArrayType = () => {\n if (Array.isArray(prefixItems)) {\n const prefixItemsTypes = prefixItems.map((itemSchema) =>\n getType(itemSchema, processedSchemas)\n )\n const itemsType = items ? getType(items, processedSchemas) : \"any\"\n return `array<[${prefixItemsTypes.join(\", \")}], ${itemsType}>`\n } else if (items) {\n const itemsType = getType(items, processedSchemas)\n return `array<${itemsType}>`\n } else {\n return \"array\"\n }\n }\n\n const inferType = () => {\n if (\n Object.hasOwn(schema, \"prefixItems\") ||\n Object.hasOwn(schema, \"items\") ||\n Object.hasOwn(schema, \"contains\")\n ) {\n return getArrayType()\n } else if (\n Object.hasOwn(schema, \"properties\") ||\n Object.hasOwn(schema, \"additionalProperties\") ||\n Object.hasOwn(schema, \"patternProperties\")\n ) {\n return \"object\"\n } else if ([\"int32\", \"int64\"].includes(schema.format)) {\n // OpenAPI 3.1.0 integer custom formats\n return \"integer\"\n } else if ([\"float\", \"double\"].includes(schema.format)) {\n // OpenAPI 3.1.0 number custom formats\n return \"number\"\n } else if (\n Object.hasOwn(schema, \"minimum\") ||\n Object.hasOwn(schema, \"maximum\") ||\n Object.hasOwn(schema, \"exclusiveMinimum\") ||\n Object.hasOwn(schema, \"exclusiveMaximum\") ||\n Object.hasOwn(schema, \"multipleOf\")\n ) {\n return \"number | integer\"\n } else if (\n Object.hasOwn(schema, \"pattern\") ||\n Object.hasOwn(schema, \"format\") ||\n Object.hasOwn(schema, \"minLength\") ||\n Object.hasOwn(schema, \"maxLength\")\n ) {\n return \"string\"\n } else if (typeof schema.const !== \"undefined\") {\n if (schema.const === null) {\n return \"null\"\n } else if (typeof schema.const === \"boolean\") {\n return \"boolean\"\n } else if (typeof schema.const === \"number\") {\n return Number.isInteger(schema.const) ? \"integer\" : \"number\"\n } else if (typeof schema.const === \"string\") {\n return \"string\"\n } else if (Array.isArray(schema.const)) {\n return \"array\"\n } else if (typeof schema.const === \"object\") {\n return \"object\"\n }\n }\n return null\n }\n\n if (schema.not && getType(schema.not) === \"any\") {\n return \"never\"\n }\n\n const typeString = Array.isArray(type)\n ? type.map((t) => (t === \"array\" ? getArrayType() : t)).join(\" | \")\n : type === \"array\"\n ? getArrayType()\n : [\"null\", \"boolean\", \"object\", \"array\", \"number\", \"string\"].includes(type)\n ? type\n : inferType()\n\n const handleCombiningKeywords = (keyword, separator) => {\n if (Array.isArray(schema[keyword])) {\n const combinedTypes = schema[keyword].map((subSchema) =>\n getType(subSchema, processedSchemas)\n )\n return `(${combinedTypes.join(separator)})`\n }\n return null\n }\n\n const oneOfString = handleCombiningKeywords(\"oneOf\", \" | \")\n const anyOfString = handleCombiningKeywords(\"anyOf\", \" | \")\n const allOfString = handleCombiningKeywords(\"allOf\", \" & \")\n\n const combinedStrings = [typeString, oneOfString, anyOfString, allOfString]\n .filter(Boolean)\n .join(\" | \")\n\n processedSchemas.delete(schema)\n\n return combinedStrings || \"any\"\n}\n\nexport const isBooleanJSONSchema = (schema) => typeof schema === \"boolean\"\n\nexport const hasKeyword = (schema, keyword) =>\n schema !== null &&\n typeof schema === \"object\" &&\n Object.hasOwn(schema, keyword)\n\nexport const isExpandable = (schema) => {\n const fn = useFn()\n\n return (\n schema?.$schema ||\n schema?.$vocabulary ||\n schema?.$id ||\n schema?.$anchor ||\n schema?.$dynamicAnchor ||\n schema?.$ref ||\n schema?.$dynamicRef ||\n schema?.$defs ||\n schema?.$comment ||\n schema?.allOf ||\n schema?.anyOf ||\n schema?.oneOf ||\n fn.hasKeyword(schema, \"not\") ||\n fn.hasKeyword(schema, \"if\") ||\n fn.hasKeyword(schema, \"then\") ||\n fn.hasKeyword(schema, \"else\") ||\n schema?.dependentSchemas ||\n schema?.prefixItems ||\n fn.hasKeyword(schema, \"items\") ||\n fn.hasKeyword(schema, \"contains\") ||\n schema?.properties ||\n schema?.patternProperties ||\n fn.hasKeyword(schema, \"additionalProperties\") ||\n fn.hasKeyword(schema, \"propertyNames\") ||\n fn.hasKeyword(schema, \"unevaluatedItems\") ||\n fn.hasKeyword(schema, \"unevaluatedProperties\") ||\n schema?.description ||\n schema?.enum ||\n fn.hasKeyword(schema, \"const\") ||\n fn.hasKeyword(schema, \"contentSchema\") ||\n fn.hasKeyword(schema, \"default\")\n )\n}\n\nexport const stringify = (value) => {\n if (\n value === null ||\n [\"number\", \"bigint\", \"boolean\"].includes(typeof value)\n ) {\n return String(value)\n }\n\n if (Array.isArray(value)) {\n return `[${value.map(stringify).join(\", \")}]`\n }\n\n return JSON.stringify(value)\n}\n\nconst stringifyConstraintMultipleOf = (schema) => {\n if (typeof schema?.multipleOf !== \"number\") return null\n if (schema.multipleOf <= 0) return null\n if (schema.multipleOf === 1) return null\n\n const { multipleOf } = schema\n\n if (Number.isInteger(multipleOf)) {\n return `multiple of ${multipleOf}`\n }\n\n const decimalPlaces = multipleOf.toString().split(\".\")[1].length\n const factor = 10 ** decimalPlaces\n const numerator = multipleOf * factor\n const denominator = factor\n return `multiple of ${numerator}/${denominator}`\n}\n\nconst stringifyConstraintNumberRange = (schema) => {\n const minimum = schema?.minimum\n const maximum = schema?.maximum\n const exclusiveMinimum = schema?.exclusiveMinimum\n const exclusiveMaximum = schema?.exclusiveMaximum\n const hasMinimum = typeof minimum === \"number\"\n const hasMaximum = typeof maximum === \"number\"\n const hasExclusiveMinimum = typeof exclusiveMinimum === \"number\"\n const hasExclusiveMaximum = typeof exclusiveMaximum === \"number\"\n const isMinExclusive = hasExclusiveMinimum && minimum < exclusiveMinimum\n const isMaxExclusive = hasExclusiveMaximum && maximum > exclusiveMaximum\n\n if (hasMinimum && hasMaximum) {\n const minSymbol = isMinExclusive ? \"(\" : \"[\"\n const maxSymbol = isMaxExclusive ? \")\" : \"]\"\n const minValue = isMinExclusive ? exclusiveMinimum : minimum\n const maxValue = isMaxExclusive ? exclusiveMaximum : maximum\n return `${minSymbol}${minValue}, ${maxValue}${maxSymbol}`\n }\n if (hasMinimum) {\n const minSymbol = isMinExclusive ? \">\" : \"≥\"\n const minValue = isMinExclusive ? exclusiveMinimum : minimum\n return `${minSymbol} ${minValue}`\n }\n if (hasMaximum) {\n const maxSymbol = isMaxExclusive ? \"<\" : \"≤\"\n const maxValue = isMaxExclusive ? exclusiveMaximum : maximum\n return `${maxSymbol} ${maxValue}`\n }\n\n return null\n}\n\nconst stringifyConstraintRange = (label, min, max) => {\n const hasMin = typeof min === \"number\"\n const hasMax = typeof max === \"number\"\n\n if (hasMin && hasMax) {\n if (min === max) {\n return `${min} ${label}`\n } else {\n return `[${min}, ${max}] ${label}`\n }\n }\n if (hasMin) {\n return `>= ${min} ${label}`\n }\n if (hasMax) {\n return `<= ${max} ${label}`\n }\n\n return null\n}\n\nexport const stringifyConstraints = (schema) => {\n const constraints = []\n\n // validation Keywords for Numeric Instances (number and integer)\n const multipleOf = stringifyConstraintMultipleOf(schema)\n if (multipleOf !== null) {\n constraints.push({ scope: \"number\", value: multipleOf })\n }\n const numberRange = stringifyConstraintNumberRange(schema)\n if (numberRange !== null) {\n constraints.push({ scope: \"number\", value: numberRange })\n }\n\n // vocabularies for Semantic Content With \"format\"\n if (schema?.format) {\n constraints.push({ scope: \"string\", value: schema.format })\n }\n\n // validation Keywords for Strings\n const stringRange = stringifyConstraintRange(\n \"characters\",\n schema?.minLength,\n schema?.maxLength\n )\n if (stringRange !== null) {\n constraints.push({ scope: \"string\", value: stringRange })\n }\n if (schema?.pattern) {\n constraints.push({ scope: \"string\", value: `matches ${schema?.pattern}` })\n }\n\n // vocabulary for the Contents of String-Encoded Data\n if (schema?.contentMediaType) {\n constraints.push({\n scope: \"string\",\n value: `media type: ${schema.contentMediaType}`,\n })\n }\n if (schema?.contentEncoding) {\n constraints.push({\n scope: \"string\",\n value: `encoding: ${schema.contentEncoding}`,\n })\n }\n\n // validation Keywords for Arrays\n const arrayRange = stringifyConstraintRange(\n schema?.hasUniqueItems ? \"unique items\" : \"items\",\n schema?.minItems,\n schema?.maxItems\n )\n if (arrayRange !== null) {\n constraints.push({ scope: \"array\", value: arrayRange })\n }\n const containsRange = stringifyConstraintRange(\n \"contained items\",\n schema?.minContains,\n schema?.maxContains\n )\n if (containsRange !== null) {\n constraints.push({ scope: \"array\", value: containsRange })\n }\n\n // validation Keywords for Objects\n const objectRange = stringifyConstraintRange(\n \"properties\",\n schema?.minProperties,\n schema?.maxProperties\n )\n if (objectRange !== null) {\n constraints.push({ scope: \"object\", value: objectRange })\n }\n\n return constraints\n}\n\nexport const getDependentRequired = (propertyName, schema) => {\n if (!schema?.dependentRequired) return []\n\n return Array.from(\n Object.entries(schema.dependentRequired).reduce((acc, [prop, list]) => {\n if (!Array.isArray(list)) return acc\n if (!list.includes(propertyName)) return acc\n\n acc.add(prop)\n\n return acc\n }, new Set())\n )\n}\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport JSONSchema from \"./components/JSONSchema/JSONSchema\"\nimport Keyword$schema from \"./components/keywords/$schema\"\nimport Keyword$vocabulary from \"./components/keywords/$vocabulary/$vocabulary\"\nimport Keyword$id from \"./components/keywords/$id\"\nimport Keyword$anchor from \"./components/keywords/$anchor\"\nimport Keyword$dynamicAnchor from \"./components/keywords/$dynamicAnchor\"\nimport Keyword$ref from \"./components/keywords/$ref\"\nimport Keyword$dynamicRef from \"./components/keywords/$dynamicRef\"\nimport Keyword$defs from \"./components/keywords/$defs\"\nimport Keyword$comment from \"./components/keywords/$comment\"\nimport KeywordAllOf from \"./components/keywords/AllOf\"\nimport KeywordAnyOf from \"./components/keywords/AnyOf\"\nimport KeywordOneOf from \"./components/keywords/OneOf\"\nimport KeywordNot from \"./components/keywords/Not\"\nimport KeywordIf from \"./components/keywords/If\"\nimport KeywordThen from \"./components/keywords/Then\"\nimport KeywordElse from \"./components/keywords/Else\"\nimport KeywordDependentSchemas from \"./components/keywords/DependentSchemas\"\nimport KeywordPrefixItems from \"./components/keywords/PrefixItems\"\nimport KeywordItems from \"./components/keywords/Items\"\nimport KeywordContains from \"./components/keywords/Contains\"\nimport KeywordProperties from \"./components/keywords/Properties/Properties\"\nimport KeywordPatternProperties from \"./components/keywords/PatternProperties/PatternProperties\"\nimport KeywordAdditionalProperties from \"./components/keywords/AdditionalProperties\"\nimport KeywordPropertyNames from \"./components/keywords/PropertyNames\"\nimport KeywordUnevaluatedItems from \"./components/keywords/UnevaluatedItems\"\nimport KeywordUnevaluatedProperties from \"./components/keywords/UnevaluatedProperties\"\nimport KeywordType from \"./components/keywords/Type\"\nimport KeywordEnum from \"./components/keywords/Enum/Enum\"\nimport KeywordConst from \"./components/keywords/Const\"\nimport KeywordConstraint from \"./components/keywords/Constraint/Constraint\"\nimport KeywordDependentRequired from \"./components/keywords/DependentRequired/DependentRequired\"\nimport KeywordContentSchema from \"./components/keywords/ContentSchema\"\nimport KeywordTitle from \"./components/keywords/Title/Title\"\nimport KeywordDescription from \"./components/keywords/Description/Description\"\nimport KeywordDefault from \"./components/keywords/Default\"\nimport KeywordDeprecated from \"./components/keywords/Deprecated\"\nimport KeywordReadOnly from \"./components/keywords/ReadOnly\"\nimport KeywordWriteOnly from \"./components/keywords/WriteOnly\"\nimport Accordion from \"./components/Accordion/Accordion\"\nimport ExpandDeepButton from \"./components/ExpandDeepButton/ExpandDeepButton\"\nimport ChevronRightIcon from \"./components/icons/ChevronRight\"\nimport { JSONSchemaContext } from \"./context\"\nimport {\n getTitle,\n isBooleanJSONSchema,\n upperFirst,\n getType,\n hasKeyword,\n isExpandable,\n stringify,\n stringifyConstraints,\n getDependentRequired,\n} from \"./fn\"\n\nexport const withJSONSchemaContext = (Component, overrides = {}) => {\n const value = {\n components: {\n JSONSchema,\n Keyword$schema,\n Keyword$vocabulary,\n Keyword$id,\n Keyword$anchor,\n Keyword$dynamicAnchor,\n Keyword$ref,\n Keyword$dynamicRef,\n Keyword$defs,\n Keyword$comment,\n KeywordAllOf,\n KeywordAnyOf,\n KeywordOneOf,\n KeywordNot,\n KeywordIf,\n KeywordThen,\n KeywordElse,\n KeywordDependentSchemas,\n KeywordPrefixItems,\n KeywordItems,\n KeywordContains,\n KeywordProperties,\n KeywordPatternProperties,\n KeywordAdditionalProperties,\n KeywordPropertyNames,\n KeywordUnevaluatedItems,\n KeywordUnevaluatedProperties,\n KeywordType,\n KeywordEnum,\n KeywordConst,\n KeywordConstraint,\n KeywordDependentRequired,\n KeywordContentSchema,\n KeywordTitle,\n KeywordDescription,\n KeywordDefault,\n KeywordDeprecated,\n KeywordReadOnly,\n KeywordWriteOnly,\n Accordion,\n ExpandDeepButton,\n ChevronRightIcon,\n ...overrides.components,\n },\n config: {\n default$schema: \"https://json-schema.org/draft/2020-12/schema\",\n /**\n * Defines an upper exclusive boundary of the level range for automatic expansion.\n *\n * 0 -> do nothing\n * 1 -> [0]...(1)\n * 2 -> [0]...(2)\n * 3 -> [0]...(3)\n */\n defaultExpandedLevels: 0, // 2 = 0...2\n ...overrides.config,\n },\n fn: {\n upperFirst,\n getTitle,\n getType,\n isBooleanJSONSchema,\n hasKeyword,\n isExpandable,\n stringify,\n stringifyConstraints,\n getDependentRequired,\n ...overrides.fn,\n },\n }\n\n const HOC = (props) => (\n \n \n \n )\n HOC.contexts = {\n JSONSchemaContext,\n }\n HOC.displayName = Component.displayName\n\n return HOC\n}\n","/**\n * @prettier\n */\nimport { useContext } from \"react\"\n\nimport {\n JSONSchemaContext,\n JSONSchemaLevelContext,\n JSONSchemaDeepExpansionContext,\n JSONSchemaCyclesContext,\n} from \"./context\"\n\nexport const useConfig = () => {\n const { config } = useContext(JSONSchemaContext)\n return config\n}\n\nexport const useComponent = (componentName) => {\n const { components } = useContext(JSONSchemaContext)\n return components[componentName] || null\n}\n\nexport const useFn = (fnName = undefined) => {\n const { fn } = useContext(JSONSchemaContext)\n\n return typeof fnName !== \"undefined\" ? fn[fnName] : fn\n}\n\nexport const useLevel = () => {\n const level = useContext(JSONSchemaLevelContext)\n\n return [level, level + 1]\n}\n\nexport const useIsEmbedded = () => {\n const [level] = useLevel()\n\n return level > 0\n}\n\nexport const useIsExpanded = () => {\n const [level] = useLevel()\n const { defaultExpandedLevels } = useConfig()\n\n return defaultExpandedLevels - level > 0\n}\n\nexport const useIsExpandedDeeply = () => {\n return useContext(JSONSchemaDeepExpansionContext)\n}\n\nexport const useRenderedSchemas = (schema = undefined) => {\n if (typeof schema === \"undefined\") {\n return useContext(JSONSchemaCyclesContext)\n }\n\n const renderedSchemas = useContext(JSONSchemaCyclesContext)\n return new Set([...renderedSchemas, schema])\n}\nexport const useIsCircular = (schema) => {\n const renderedSchemas = useRenderedSchemas()\n return renderedSchemas.has(schema)\n}\n","/**\n * @prettier\n */\nimport JSONSchema from \"./components/JSONSchema/JSONSchema\"\nimport Keyword$schema from \"./components/keywords/$schema\"\nimport Keyword$vocabulary from \"./components/keywords/$vocabulary/$vocabulary\"\nimport Keyword$id from \"./components/keywords/$id\"\nimport Keyword$anchor from \"./components/keywords/$anchor\"\nimport Keyword$dynamicAnchor from \"./components/keywords/$dynamicAnchor\"\nimport Keyword$ref from \"./components/keywords/$ref\"\nimport Keyword$dynamicRef from \"./components/keywords/$dynamicRef\"\nimport Keyword$defs from \"./components/keywords/$defs\"\nimport Keyword$comment from \"./components/keywords/$comment\"\nimport KeywordAllOf from \"./components/keywords/AllOf\"\nimport KeywordAnyOf from \"./components/keywords/AnyOf\"\nimport KeywordOneOf from \"./components/keywords/OneOf\"\nimport KeywordNot from \"./components/keywords/Not\"\nimport KeywordIf from \"./components/keywords/If\"\nimport KeywordThen from \"./components/keywords/Then\"\nimport KeywordElse from \"./components/keywords/Else\"\nimport KeywordDependentSchemas from \"./components/keywords/DependentSchemas\"\nimport KeywordPrefixItems from \"./components/keywords/PrefixItems\"\nimport KeywordItems from \"./components/keywords/Items\"\nimport KeywordContains from \"./components/keywords/Contains\"\nimport KeywordProperties from \"./components/keywords/Properties/Properties\"\nimport KeywordPatternProperties from \"./components/keywords/PatternProperties/PatternProperties\"\nimport KeywordAdditionalProperties from \"./components/keywords/AdditionalProperties\"\nimport KeywordPropertyNames from \"./components/keywords/PropertyNames\"\nimport KeywordUnevaluatedItems from \"./components/keywords/UnevaluatedItems\"\nimport KeywordUnevaluatedProperties from \"./components/keywords/UnevaluatedProperties\"\nimport KeywordType from \"./components/keywords/Type\"\nimport KeywordEnum from \"./components/keywords/Enum/Enum\"\nimport KeywordConst from \"./components/keywords/Const\"\nimport KeywordConstraint from \"./components/keywords/Constraint/Constraint\"\nimport KeywordDependentRequired from \"./components/keywords/DependentRequired/DependentRequired\"\nimport KeywordContentSchema from \"./components/keywords/ContentSchema\"\nimport KeywordTitle from \"./components/keywords/Title/Title\"\nimport KeywordDescription from \"./components/keywords/Description/Description\"\nimport KeywordDefault from \"./components/keywords/Default\"\nimport KeywordDeprecated from \"./components/keywords/Deprecated\"\nimport KeywordReadOnly from \"./components/keywords/ReadOnly\"\nimport KeywordWriteOnly from \"./components/keywords/WriteOnly\"\nimport Accordion from \"./components/Accordion/Accordion\"\nimport ExpandDeepButton from \"./components/ExpandDeepButton/ExpandDeepButton\"\nimport ChevronRightIcon from \"./components/icons/ChevronRight\"\nimport { upperFirst, hasKeyword, isExpandable } from \"./fn\"\nimport {\n sampleFromSchema,\n sampleFromSchemaGeneric,\n createXMLExample,\n memoizedSampleFromSchema,\n memoizedCreateXMLExample,\n encoderAPI,\n mediaTypeAPI,\n formatAPI,\n} from \"./samples-extensions/fn/index\"\nimport { JSONSchemaDeepExpansionContext } from \"./context\"\nimport { useFn, useConfig, useComponent, useIsExpandedDeeply } from \"./hooks\"\nimport { withJSONSchemaContext } from \"./hoc\"\n\nconst JSONSchema202012Plugin = () => ({\n components: {\n JSONSchema202012: JSONSchema,\n JSONSchema202012Keyword$schema: Keyword$schema,\n JSONSchema202012Keyword$vocabulary: Keyword$vocabulary,\n JSONSchema202012Keyword$id: Keyword$id,\n JSONSchema202012Keyword$anchor: Keyword$anchor,\n JSONSchema202012Keyword$dynamicAnchor: Keyword$dynamicAnchor,\n JSONSchema202012Keyword$ref: Keyword$ref,\n JSONSchema202012Keyword$dynamicRef: Keyword$dynamicRef,\n JSONSchema202012Keyword$defs: Keyword$defs,\n JSONSchema202012Keyword$comment: Keyword$comment,\n JSONSchema202012KeywordAllOf: KeywordAllOf,\n JSONSchema202012KeywordAnyOf: KeywordAnyOf,\n JSONSchema202012KeywordOneOf: KeywordOneOf,\n JSONSchema202012KeywordNot: KeywordNot,\n JSONSchema202012KeywordIf: KeywordIf,\n JSONSchema202012KeywordThen: KeywordThen,\n JSONSchema202012KeywordElse: KeywordElse,\n JSONSchema202012KeywordDependentSchemas: KeywordDependentSchemas,\n JSONSchema202012KeywordPrefixItems: KeywordPrefixItems,\n JSONSchema202012KeywordItems: KeywordItems,\n JSONSchema202012KeywordContains: KeywordContains,\n JSONSchema202012KeywordProperties: KeywordProperties,\n JSONSchema202012KeywordPatternProperties: KeywordPatternProperties,\n JSONSchema202012KeywordAdditionalProperties: KeywordAdditionalProperties,\n JSONSchema202012KeywordPropertyNames: KeywordPropertyNames,\n JSONSchema202012KeywordUnevaluatedItems: KeywordUnevaluatedItems,\n JSONSchema202012KeywordUnevaluatedProperties: KeywordUnevaluatedProperties,\n JSONSchema202012KeywordType: KeywordType,\n JSONSchema202012KeywordEnum: KeywordEnum,\n JSONSchema202012KeywordConst: KeywordConst,\n JSONSchema202012KeywordConstraint: KeywordConstraint,\n JSONSchema202012KeywordDependentRequired: KeywordDependentRequired,\n JSONSchema202012KeywordContentSchema: KeywordContentSchema,\n JSONSchema202012KeywordTitle: KeywordTitle,\n JSONSchema202012KeywordDescription: KeywordDescription,\n JSONSchema202012KeywordDefault: KeywordDefault,\n JSONSchema202012KeywordDeprecated: KeywordDeprecated,\n JSONSchema202012KeywordReadOnly: KeywordReadOnly,\n JSONSchema202012KeywordWriteOnly: KeywordWriteOnly,\n JSONSchema202012Accordion: Accordion,\n JSONSchema202012ExpandDeepButton: ExpandDeepButton,\n JSONSchema202012ChevronRightIcon: ChevronRightIcon,\n withJSONSchema202012Context: withJSONSchemaContext,\n JSONSchema202012DeepExpansionContext: () => JSONSchemaDeepExpansionContext,\n },\n fn: {\n upperFirst,\n jsonSchema202012: {\n isExpandable,\n hasKeyword,\n useFn,\n useConfig,\n useComponent,\n useIsExpandedDeeply,\n sampleFromSchema,\n sampleFromSchemaGeneric,\n sampleEncoderAPI: encoderAPI,\n sampleFormatAPI: formatAPI,\n sampleMediaTypeAPI: mediaTypeAPI,\n createXMLExample,\n memoizedSampleFromSchema,\n memoizedCreateXMLExample,\n },\n },\n})\n\nexport default JSONSchema202012Plugin\n","/**\n * @prettier\n */\nimport PropTypes from \"prop-types\"\n\nexport const objectSchema = PropTypes.object\n\nexport const booleanSchema = PropTypes.bool\n\nexport const schema = PropTypes.oneOfType([objectSchema, booleanSchema])\n","/**\n * @prettier\n */\n\nimport EncoderRegistry from \"core/plugins/json-schema-2020-12/samples-extensions/fn/class/EncoderRegistry\"\n\nconst registry = new EncoderRegistry()\n\nconst encoderAPI = (encodingName, encoder) => {\n if (typeof encoder === \"function\") {\n return registry.register(encodingName, encoder)\n } else if (encoder === null) {\n return registry.unregister(encodingName)\n }\n\n return registry.get(encodingName)\n}\nencoderAPI.getDefaults = () => registry.defaults\n\nexport default encoderAPI\n","/**\n * @prettier\n */\n\nimport Registry from \"../class/Registry\"\n\nconst registry = new Registry()\n\nconst formatAPI = (format, generator) => {\n if (typeof generator === \"function\") {\n return registry.register(format, generator)\n } else if (generator === null) {\n return registry.unregister(format)\n }\n\n return registry.get(format)\n}\n\nexport default formatAPI\n","/**\n * @prettier\n */\n\nimport MediaTypeRegistry from \"../class/MediaTypeRegistry\"\n\nconst registry = new MediaTypeRegistry()\n\nconst mediaTypeAPI = (mediaType, generator) => {\n if (typeof generator === \"function\") {\n return registry.register(mediaType, generator)\n } else if (generator === null) {\n return registry.unregister(mediaType)\n }\n\n const mediaTypeNoParams = mediaType.split(\";\").at(0)\n const topLevelMediaType = `${mediaTypeNoParams.split(\"/\").at(0)}/*`\n\n return (\n registry.get(mediaType) ||\n registry.get(mediaTypeNoParams) ||\n registry.get(topLevelMediaType)\n )\n}\nmediaTypeAPI.getDefaults = () => registry.defaults\n\nexport default mediaTypeAPI\n","/**\n * @prettier\n */\nimport Registry from \"./Registry\"\nimport encode7bit from \"../encoders/7bit\"\nimport encode8bit from \"../encoders/8bit\"\nimport encodeBinary from \"../encoders/binary\"\nimport encodeQuotedPrintable from \"../encoders/quoted-printable\"\nimport encodeBase16 from \"../encoders/base16\"\nimport encodeBase32 from \"../encoders/base32\"\nimport encodeBase64 from \"../encoders/base64\"\n\nclass EncoderRegistry extends Registry {\n #defaults = {\n \"7bit\": encode7bit,\n \"8bit\": encode8bit,\n binary: encodeBinary,\n \"quoted-printable\": encodeQuotedPrintable,\n base16: encodeBase16,\n base32: encodeBase32,\n base64: encodeBase64,\n }\n\n data = { ...this.#defaults }\n\n get defaults() {\n return { ...this.#defaults }\n }\n}\n\nexport default EncoderRegistry\n","/**\n * @prettier\n */\nimport Registry from \"./Registry\"\nimport textMediaTypesGenerators from \"../generators/media-types/text\"\nimport imageMediaTypesGenerators from \"../generators/media-types/image\"\nimport audioMediaTypesGenerators from \"../generators/media-types/audio\"\nimport videoMediaTypesGenerators from \"../generators/media-types/video\"\nimport applicationMediaTypesGenerators from \"../generators/media-types/application\"\n\nclass MediaTypeRegistry extends Registry {\n #defaults = {\n ...textMediaTypesGenerators,\n ...imageMediaTypesGenerators,\n ...audioMediaTypesGenerators,\n ...videoMediaTypesGenerators,\n ...applicationMediaTypesGenerators,\n }\n\n data = { ...this.#defaults }\n\n get defaults() {\n return { ...this.#defaults }\n }\n}\n\nexport default MediaTypeRegistry\n","/**\n * @prettier\n */\nclass Registry {\n data = {}\n\n register(name, value) {\n this.data[name] = value\n }\n\n unregister(name) {\n if (typeof name === \"undefined\") {\n this.data = {}\n } else {\n delete this.data[name]\n }\n }\n\n get(name) {\n return this.data[name]\n }\n}\n\nexport default Registry\n","/**\n * @prettier\n */\nexport const SCALAR_TYPES = [\"number\", \"integer\", \"string\", \"boolean\", \"null\"]\n\nexport const ALL_TYPES = [\"array\", \"object\", ...SCALAR_TYPES]\n","/**\n * @prettier\n */\nimport { isJSONSchemaObject } from \"./predicates\"\n\n/**\n * Precedence of keywords that provides author defined values (top of the list = higher priority)\n *\n * ### examples\n * Array containing example values for the item defined by the schema.\n * Not guaranteed to be valid or invalid against the schema\n *\n * ### default\n * Default value for an item defined by the schema.\n * Is expected to be a valid instance of the schema.\n *\n * ### example\n * Deprecated. Part of OpenAPI 3.1.0 Schema Object dialect.\n * Represents single example. Equivalent of `examples` keywords\n * with single item.\n */\n\nexport const hasExample = (schema) => {\n if (!isJSONSchemaObject(schema)) return false\n\n const { examples, example, default: defaultVal } = schema\n\n if (Array.isArray(examples) && examples.length >= 1) {\n return true\n }\n\n if (typeof defaultVal !== \"undefined\") {\n return true\n }\n\n return typeof example !== \"undefined\"\n}\n\nexport const extractExample = (schema) => {\n if (!isJSONSchemaObject(schema)) return null\n\n const { examples, example, default: defaultVal } = schema\n\n if (Array.isArray(examples) && examples.length >= 1) {\n return examples.at(0)\n }\n\n if (typeof defaultVal !== \"undefined\") {\n return defaultVal\n }\n\n if (typeof example !== \"undefined\") {\n return example\n }\n\n return undefined\n}\n","/**\n * @prettier\n */\nimport { normalizeArray as ensureArray } from \"core/utils\"\nimport { isBooleanJSONSchema, isJSONSchema } from \"./predicates\"\n\nconst merge = (target, source, config = {}) => {\n if (isBooleanJSONSchema(target) && target === true) return true\n if (isBooleanJSONSchema(target) && target === false) return false\n if (isBooleanJSONSchema(source) && source === true) return true\n if (isBooleanJSONSchema(source) && source === false) return false\n\n if (!isJSONSchema(target)) return source\n if (!isJSONSchema(source)) return target\n\n /**\n * Merging properties from the source object into the target object\n * only if they do not already exist in the target object.\n */\n const merged = { ...source, ...target }\n\n // merging the type keyword\n if (source.type && target.type) {\n if (Array.isArray(source.type) && typeof source.type === \"string\") {\n const mergedType = ensureArray(source.type).concat(target.type)\n merged.type = Array.from(new Set(mergedType))\n }\n }\n\n // merging required keyword\n if (Array.isArray(source.required) && Array.isArray(target.required)) {\n merged.required = [...new Set([...target.required, ...source.required])]\n }\n\n // merging properties keyword\n if (source.properties && target.properties) {\n const allPropertyNames = new Set([\n ...Object.keys(source.properties),\n ...Object.keys(target.properties),\n ])\n\n merged.properties = {}\n for (const name of allPropertyNames) {\n const sourceProperty = source.properties[name] || {}\n const targetProperty = target.properties[name] || {}\n\n if (\n (sourceProperty.readOnly && !config.includeReadOnly) ||\n (sourceProperty.writeOnly && !config.includeWriteOnly)\n ) {\n merged.required = (merged.required || []).filter((p) => p !== name)\n } else {\n merged.properties[name] = merge(targetProperty, sourceProperty, config)\n }\n }\n }\n\n // merging items keyword\n if (isJSONSchema(source.items) && isJSONSchema(target.items)) {\n merged.items = merge(target.items, source.items, config)\n }\n\n // merging contains keyword\n if (isJSONSchema(source.contains) && isJSONSchema(target.contains)) {\n merged.contains = merge(target.contains, source.contains, config)\n }\n\n // merging contentSchema keyword\n if (\n isJSONSchema(source.contentSchema) &&\n isJSONSchema(target.contentSchema)\n ) {\n merged.contentSchema = merge(\n target.contentSchema,\n source.contentSchema,\n config\n )\n }\n\n return merged\n}\n\nexport default merge\n","/**\n * @prettier\n */\nimport isPlainObject from \"lodash/isPlainObject\"\n\nexport const isBooleanJSONSchema = (schema) => {\n return typeof schema === \"boolean\"\n}\n\nexport const isJSONSchemaObject = (schema) => {\n return isPlainObject(schema)\n}\n\nexport const isJSONSchema = (schema) => {\n return isBooleanJSONSchema(schema) || isJSONSchemaObject(schema)\n}\n","/**\n * @prettier\n */\nimport randomBytes from \"randombytes\"\nimport RandExp from \"randexp\"\n\n/**\n * Some of the functions returns constants. This is due to the nature\n * of SwaggerUI expectations - provide as stable data as possible.\n *\n * In future, we may decide to randomize these function and provide\n * true random values.\n */\n\nexport const bytes = (length) => randomBytes(length)\n\nexport const randexp = (pattern) => {\n try {\n const randexpInstance = new RandExp(pattern)\n return randexpInstance.gen()\n } catch {\n // invalid regex should not cause a crash (regex syntax varies across languages)\n return \"string\"\n }\n}\n\nexport const pick = (list) => {\n return list.at(0)\n}\n\nexport const string = () => \"string\"\n\nexport const number = () => 0\n\nexport const integer = () => 0\n","/**\n * @prettier\n */\nimport { ALL_TYPES } from \"./constants\"\nimport { isJSONSchemaObject } from \"./predicates\"\nimport { pick as randomPick } from \"./random\"\nimport { hasExample, extractExample } from \"./example\"\n\nconst inferringKeywords = {\n array: [\n \"items\",\n \"prefixItems\",\n \"contains\",\n \"maxContains\",\n \"minContains\",\n \"maxItems\",\n \"minItems\",\n \"uniqueItems\",\n \"unevaluatedItems\",\n ],\n object: [\n \"properties\",\n \"additionalProperties\",\n \"patternProperties\",\n \"propertyNames\",\n \"minProperties\",\n \"maxProperties\",\n \"required\",\n \"dependentSchemas\",\n \"dependentRequired\",\n \"unevaluatedProperties\",\n ],\n string: [\n \"pattern\",\n \"format\",\n \"minLength\",\n \"maxLength\",\n \"contentEncoding\",\n \"contentMediaType\",\n \"contentSchema\",\n ],\n integer: [\n \"minimum\",\n \"maximum\",\n \"exclusiveMinimum\",\n \"exclusiveMaximum\",\n \"multipleOf\",\n ],\n}\ninferringKeywords.number = inferringKeywords.integer\n\nconst fallbackType = \"string\"\n\nconst inferTypeFromValue = (value) => {\n if (typeof value === \"undefined\") return null\n if (value === null) return \"null\"\n if (Array.isArray(value)) return \"array\"\n if (Number.isInteger(value)) return \"integer\"\n\n return typeof value\n}\n\nexport const foldType = (type) => {\n if (Array.isArray(type) && type.length >= 1) {\n if (type.includes(\"array\")) {\n return \"array\"\n } else if (type.includes(\"object\")) {\n return \"object\"\n } else {\n const pickedType = randomPick(type)\n if (ALL_TYPES.includes(pickedType)) {\n return pickedType\n }\n }\n }\n\n if (ALL_TYPES.includes(type)) {\n return type\n }\n\n return null\n}\n\nexport const inferType = (schema, processedSchemas = new WeakSet()) => {\n if (!isJSONSchemaObject(schema)) return fallbackType\n if (processedSchemas.has(schema)) return fallbackType\n\n processedSchemas.add(schema)\n\n let { type, const: constant } = schema\n type = foldType(type)\n\n // inferring type from inferring keywords\n if (typeof type !== \"string\") {\n const inferringTypes = Object.keys(inferringKeywords)\n\n interrupt: for (let i = 0; i < inferringTypes.length; i += 1) {\n const inferringType = inferringTypes[i]\n const inferringTypeKeywords = inferringKeywords[inferringType]\n\n for (let j = 0; j < inferringTypeKeywords.length; j += 1) {\n const inferringKeyword = inferringTypeKeywords[j]\n if (Object.hasOwn(schema, inferringKeyword)) {\n type = inferringType\n break interrupt\n }\n }\n }\n }\n\n // inferring type from const keyword\n if (typeof type !== \"string\" && typeof constant !== \"undefined\") {\n const constType = inferTypeFromValue(constant)\n type = typeof constType === \"string\" ? constType : type\n }\n\n // inferring type from combining schemas\n if (typeof type !== \"string\") {\n const combineTypes = (keyword) => {\n if (Array.isArray(schema[keyword])) {\n const combinedTypes = schema[keyword].map((subSchema) =>\n inferType(subSchema, processedSchemas)\n )\n return foldType(combinedTypes)\n }\n return null\n }\n\n const allOf = combineTypes(\"allOf\")\n const anyOf = combineTypes(\"anyOf\")\n const oneOf = combineTypes(\"oneOf\")\n const not = schema.not ? inferType(schema.not, processedSchemas) : null\n\n if (allOf || anyOf || oneOf || not) {\n type = foldType([allOf, anyOf, oneOf, not].filter(Boolean))\n }\n }\n\n // inferring type from example\n if (typeof type !== \"string\" && hasExample(schema)) {\n const example = extractExample(schema)\n const exampleType = inferTypeFromValue(example)\n type = typeof exampleType === \"string\" ? exampleType : type\n }\n\n processedSchemas.delete(schema)\n\n return type || fallbackType\n}\n\nexport const getType = (schema) => {\n return inferType(schema)\n}\n","/**\n * @prettier\n */\nimport { isBooleanJSONSchema, isJSONSchemaObject } from \"./predicates\"\n\nexport const fromJSONBooleanSchema = (schema) => {\n if (schema === false) {\n return { not: {} }\n }\n\n return {}\n}\n\nexport const typeCast = (schema) => {\n if (isBooleanJSONSchema(schema)) {\n return fromJSONBooleanSchema(schema)\n }\n if (!isJSONSchemaObject(schema)) {\n return {}\n }\n\n return schema\n}\n","/**\n * @prettier\n */\nconst encode7bit = (content) => Buffer.from(content).toString(\"ascii\")\n\nexport default encode7bit\n","/**\n * @prettier\n */\nconst encode8bit = (content) => Buffer.from(content).toString(\"utf8\")\n\nexport default encode8bit\n","/**\n * @prettier\n */\nconst encodeBase16 = (content) => Buffer.from(content).toString(\"hex\")\n\nexport default encodeBase16\n","/**\n * @prettier\n */\nconst encodeBase32 = (content) => {\n const utf8Value = Buffer.from(content).toString(\"utf8\")\n const base32Alphabet = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567\"\n let paddingCount = 0\n let base32Str = \"\"\n let buffer = 0\n let bufferLength = 0\n\n for (let i = 0; i < utf8Value.length; i++) {\n buffer = (buffer << 8) | utf8Value.charCodeAt(i)\n bufferLength += 8\n\n while (bufferLength >= 5) {\n base32Str += base32Alphabet.charAt((buffer >>> (bufferLength - 5)) & 31)\n bufferLength -= 5\n }\n }\n\n if (bufferLength > 0) {\n base32Str += base32Alphabet.charAt((buffer << (5 - bufferLength)) & 31)\n paddingCount = (8 - ((utf8Value.length * 8) % 5)) % 5\n }\n\n for (let i = 0; i < paddingCount; i++) {\n base32Str += \"=\"\n }\n\n return base32Str\n}\n\nexport default encodeBase32\n","/**\n * @prettier\n */\nconst encodeBase64 = (content) => Buffer.from(content).toString(\"base64\")\n\nexport default encodeBase64\n","/**\n * @prettier\n */\nconst encodeBinary = (content) => Buffer.from(content).toString(\"binary\")\n\nexport default encodeBinary\n","/**\n * @prettier\n */\nconst encodeQuotedPrintable = (content) => {\n let quotedPrintable = \"\"\n\n for (let i = 0; i < content.length; i++) {\n const charCode = content.charCodeAt(i)\n\n if (charCode === 61) {\n // ASCII content of \"=\"\n quotedPrintable += \"=3D\"\n } else if (\n (charCode >= 33 && charCode <= 60) ||\n (charCode >= 62 && charCode <= 126) ||\n charCode === 9 ||\n charCode === 32\n ) {\n quotedPrintable += content.charAt(i)\n } else if (charCode === 13 || charCode === 10) {\n quotedPrintable += \"\\r\\n\"\n } else if (charCode > 126) {\n // convert non-ASCII characters to UTF-8 and encode each byte\n const utf8 = unescape(encodeURIComponent(content.charAt(i)))\n for (let j = 0; j < utf8.length; j++) {\n quotedPrintable +=\n \"=\" + (\"0\" + utf8.charCodeAt(j).toString(16)).slice(-2).toUpperCase()\n }\n } else {\n quotedPrintable +=\n \"=\" + (\"0\" + charCode.toString(16)).slice(-2).toUpperCase()\n }\n }\n\n return quotedPrintable\n}\n\nexport default encodeQuotedPrintable\n","/**\n * @prettier\n */\nconst dateTimeGenerator = () => new Date().toISOString()\n\nexport default dateTimeGenerator\n","/**\n * @prettier\n */\nconst dateGenerator = () => new Date().toISOString().substring(0, 10)\n\nexport default dateGenerator\n","/**\n * @prettier\n */\nconst doubleGenerator = () => 0.1\n\nexport default doubleGenerator\n","/**\n * @prettier\n */\nconst durationGenerator = () => \"P3D\" // expresses a duration of 3 days\n\nexport default durationGenerator\n","/**\n * @prettier\n */\nconst emailGenerator = () => \"user@example.com\"\n\nexport default emailGenerator\n","/**\n * @prettier\n */\nconst floatGenerator = () => 0.1\n\nexport default floatGenerator\n","/**\n * @prettier\n */\nconst hostnameGenerator = () => \"example.com\"\n\nexport default hostnameGenerator\n","/**\n * @prettier\n */\nconst idnEmailGenerator = () => \"실례@example.com\"\n\nexport default idnEmailGenerator\n","/**\n * @prettier\n */\nconst idnHostnameGenerator = () => \"실례.com\"\n\nexport default idnHostnameGenerator\n","/**\n * @prettier\n */\nconst int32Generator = () => (2 ** 30) >>> 0\n\nexport default int32Generator\n","/**\n * @prettier\n */\nconst int64Generator = () => 2 ** 53 - 1\n\nexport default int64Generator\n","/**\n * @prettier\n */\nconst ipv4Generator = () => \"198.51.100.42\"\n\nexport default ipv4Generator\n","/**\n * @prettier\n */\nconst ipv6Generator = () => \"2001:0db8:5b96:0000:0000:426f:8e17:642a\"\n\nexport default ipv6Generator\n","/**\n * @prettier\n */\nconst iriReferenceGenerator = () => \"path/실례.html\"\n\nexport default iriReferenceGenerator\n","/**\n * @prettier\n */\nconst iriGenerator = () => \"https://실례.com/\"\n\nexport default iriGenerator\n","/**\n * @prettier\n */\nconst jsonPointerGenerator = () => \"/a/b/c\"\n\nexport default jsonPointerGenerator\n","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_string_raw_bd16f4a0__[\"default\"] });","/**\n * @prettier\n */\nimport { bytes } from \"../../core/random\"\n\n// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types\nconst applicationMediaTypesGenerators = {\n \"application/json\": () => '{\"key\":\"value\"}',\n \"application/ld+json\": () => '{\"name\": \"John Doe\"}',\n \"application/x-httpd-php\": () => \"Hello World!

    '; ?>\",\n \"application/rtf\": () => String.raw`{\\rtf1\\adeflang1025\\ansi\\ansicpg1252\\uc1`,\n \"application/x-sh\": () => 'echo \"Hello World!\"',\n \"application/xhtml+xml\": () => \"

    content

    \",\n \"application/*\": () => bytes(25).toString(\"binary\"),\n}\n\nexport default applicationMediaTypesGenerators\n","/**\n * @prettier\n */\nimport { bytes } from \"../../core/random\"\n\nconst audioMediaTypesGenerators = {\n \"audio/*\": () => bytes(25).toString(\"binary\"),\n}\n\nexport default audioMediaTypesGenerators\n","/**\n * @prettier\n */\nimport { bytes } from \"../../core/random\"\n\nconst imageMediaTypesGenerators = {\n \"image/*\": () => bytes(25).toString(\"binary\"),\n}\n\nexport default imageMediaTypesGenerators\n","/**\n * @prettier\n */\n\n// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types\nconst textMediaTypesGenerators = {\n \"text/plain\": () => \"string\",\n \"text/css\": () => \".selector { border: 1px solid red }\",\n \"text/csv\": () => \"value1,value2,value3\",\n \"text/html\": () => \"

    content

    \",\n \"text/calendar\": () => \"BEGIN:VCALENDAR\",\n \"text/javascript\": () => \"console.dir('Hello world!');\",\n \"text/xml\": () => 'John Doe',\n \"text/*\": () => \"string\",\n}\n\nexport default textMediaTypesGenerators\n","/**\n * @prettier\n */\nimport { bytes } from \"../../core/random\"\n\nconst videoMediaTypesGenerators = {\n \"video/*\": () => bytes(25).toString(\"binary\"),\n}\n\nexport default videoMediaTypesGenerators\n","/**\n * @prettier\n */\nconst passwordGenerator = () => \"********\"\n\nexport default passwordGenerator\n","/**\n * @prettier\n */\nconst regexGenerator = () => \"^[a-z]+$\"\n\nexport default regexGenerator\n","/**\n * @prettier\n */\nconst relativeJsonPointerGenerator = () => \"1/0\"\n\nexport default relativeJsonPointerGenerator\n","/**\n * @prettier\n */\nconst timeGenerator = () => new Date().toISOString().substring(11)\n\nexport default timeGenerator\n","/**\n * @prettier\n */\nconst uriReferenceGenerator = () => \"path/index.html\"\n\nexport default uriReferenceGenerator\n","/**\n * @prettier\n */\nconst uriTemplateGenerator = () =>\n \"https://example.com/dictionary/{term:1}/{term}\"\n\nexport default uriTemplateGenerator\n","/**\n * @prettier\n */\nconst uriGenerator = () => \"https://example.com/\"\n\nexport default uriGenerator\n","/**\n * @prettier\n */\nconst uuidGenerator = () => \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"\n\nexport default uuidGenerator\n","/**\n * @prettier\n */\nimport XML from \"xml\"\nimport isEmpty from \"lodash/isEmpty\"\nimport isPlainObject from \"lodash/isPlainObject\"\n\nimport { objectify, normalizeArray } from \"core/utils\"\nimport memoizeN from \"../../../../../helpers/memoizeN\"\nimport typeMap from \"./types/index\"\nimport { getType } from \"./core/type\"\nimport { typeCast } from \"./core/utils\"\nimport { hasExample, extractExample } from \"./core/example\"\nimport { pick as randomPick } from \"./core/random\"\nimport merge from \"./core/merge\"\nimport { isBooleanJSONSchema, isJSONSchemaObject } from \"./core/predicates\"\n\nexport const sampleFromSchemaGeneric = (\n schema,\n config = {},\n exampleOverride = undefined,\n respectXML = false\n) => {\n if (typeof schema?.toJS === \"function\") schema = schema.toJS()\n schema = typeCast(schema)\n\n let usePlainValue = exampleOverride !== undefined || hasExample(schema)\n // first check if there is the need of combining this schema with others required by allOf\n const hasOneOf =\n !usePlainValue && Array.isArray(schema.oneOf) && schema.oneOf.length > 0\n const hasAnyOf =\n !usePlainValue && Array.isArray(schema.anyOf) && schema.anyOf.length > 0\n if (!usePlainValue && (hasOneOf || hasAnyOf)) {\n const schemaToAdd = typeCast(\n hasOneOf ? randomPick(schema.oneOf) : randomPick(schema.anyOf)\n )\n schema = merge(schema, schemaToAdd, config)\n if (!schema.xml && schemaToAdd.xml) {\n schema.xml = schemaToAdd.xml\n }\n if (hasExample(schema) && hasExample(schemaToAdd)) {\n usePlainValue = true\n }\n }\n const _attr = {}\n let { xml, properties, additionalProperties, items, contains } = schema || {}\n let type = getType(schema)\n let { includeReadOnly, includeWriteOnly } = config\n xml = xml || {}\n let { name, prefix, namespace } = xml\n let displayName\n let res = {}\n\n if (!Object.hasOwn(schema, \"type\")) {\n schema.type = type\n }\n\n // set xml naming and attributes\n if (respectXML) {\n name = name || \"notagname\"\n // add prefix to name if exists\n displayName = (prefix ? `${prefix}:` : \"\") + name\n if (namespace) {\n //add prefix to namespace if exists\n let namespacePrefix = prefix ? `xmlns:${prefix}` : \"xmlns\"\n _attr[namespacePrefix] = namespace\n }\n }\n\n // init xml default response sample obj\n if (respectXML) {\n res[displayName] = []\n }\n\n // add to result helper init for xml or json\n const props = objectify(properties)\n let addPropertyToResult\n let propertyAddedCounter = 0\n\n const hasExceededMaxProperties = () =>\n Number.isInteger(schema.maxProperties) &&\n schema.maxProperties > 0 &&\n propertyAddedCounter >= schema.maxProperties\n\n const requiredPropertiesToAdd = () => {\n if (!Array.isArray(schema.required) || schema.required.length === 0) {\n return 0\n }\n let addedCount = 0\n if (respectXML) {\n schema.required.forEach(\n (key) => (addedCount += res[key] === undefined ? 0 : 1)\n )\n } else {\n schema.required.forEach((key) => {\n addedCount +=\n res[displayName]?.find((x) => x[key] !== undefined) === undefined\n ? 0\n : 1\n })\n }\n return schema.required.length - addedCount\n }\n\n const isOptionalProperty = (propName) => {\n if (!Array.isArray(schema.required)) return true\n if (schema.required.length === 0) return true\n\n return !schema.required.includes(propName)\n }\n\n const canAddProperty = (propName) => {\n if (!(Number.isInteger(schema.maxProperties) && schema.maxProperties > 0)) {\n return true\n }\n if (hasExceededMaxProperties()) {\n return false\n }\n if (!isOptionalProperty(propName)) {\n return true\n }\n return (\n schema.maxProperties - propertyAddedCounter - requiredPropertiesToAdd() >\n 0\n )\n }\n\n if (respectXML) {\n addPropertyToResult = (propName, overrideE = undefined) => {\n if (schema && props[propName]) {\n // case it is a xml attribute\n props[propName].xml = props[propName].xml || {}\n\n if (props[propName].xml.attribute) {\n const enumAttrVal = Array.isArray(props[propName].enum)\n ? randomPick(props[propName].enum)\n : undefined\n if (hasExample(props[propName])) {\n _attr[props[propName].xml.name || propName] = extractExample(\n props[propName]\n )\n } else if (enumAttrVal !== undefined) {\n _attr[props[propName].xml.name || propName] = enumAttrVal\n } else {\n const propSchema = typeCast(props[propName])\n const propSchemaType = getType(propSchema)\n const attrName = props[propName].xml.name || propName\n _attr[attrName] = typeMap[propSchemaType](propSchema)\n }\n\n return\n }\n props[propName].xml.name = props[propName].xml.name || propName\n } else if (!props[propName] && additionalProperties !== false) {\n // case only additionalProperty that is not defined in schema\n props[propName] = {\n xml: {\n name: propName,\n },\n }\n }\n\n let t = sampleFromSchemaGeneric(\n props[propName],\n config,\n overrideE,\n respectXML\n )\n if (!canAddProperty(propName)) {\n return\n }\n\n propertyAddedCounter++\n if (Array.isArray(t)) {\n res[displayName] = res[displayName].concat(t)\n } else {\n res[displayName].push(t)\n }\n }\n } else {\n addPropertyToResult = (propName, overrideE) => {\n if (!canAddProperty(propName)) {\n return\n }\n if (\n isPlainObject(schema.discriminator?.mapping) &&\n schema.discriminator.propertyName === propName &&\n typeof schema.$$ref === \"string\"\n ) {\n for (const pair in schema.discriminator.mapping) {\n if (schema.$$ref.search(schema.discriminator.mapping[pair]) !== -1) {\n res[propName] = pair\n break\n }\n }\n } else {\n res[propName] = sampleFromSchemaGeneric(\n props[propName],\n config,\n overrideE,\n respectXML\n )\n }\n propertyAddedCounter++\n }\n }\n\n // check for plain value and if found use it to generate sample from it\n if (usePlainValue) {\n let sample\n if (exampleOverride !== undefined) {\n sample = exampleOverride\n } else {\n sample = extractExample(schema)\n }\n\n // if json just return\n if (!respectXML) {\n // spacial case yaml parser can not know about\n if (typeof sample === \"number\" && type === \"string\") {\n return `${sample}`\n }\n // return if sample does not need any parsing\n if (typeof sample !== \"string\" || type === \"string\") {\n return sample\n }\n // check if sample is parsable or just a plain string\n try {\n return JSON.parse(sample)\n } catch {\n // sample is just plain string return it\n return sample\n }\n }\n\n // generate xml sample recursively for array case\n if (type === \"array\") {\n if (!Array.isArray(sample)) {\n if (typeof sample === \"string\") {\n return sample\n }\n sample = [sample]\n }\n\n let itemSamples = []\n\n if (isJSONSchemaObject(items)) {\n items.xml = items.xml || xml || {}\n items.xml.name = items.xml.name || xml.name\n itemSamples = sample.map((s) =>\n sampleFromSchemaGeneric(items, config, s, respectXML)\n )\n }\n\n if (isJSONSchemaObject(contains)) {\n contains.xml = contains.xml || xml || {}\n contains.xml.name = contains.xml.name || xml.name\n itemSamples = [\n sampleFromSchemaGeneric(contains, config, undefined, respectXML),\n ...itemSamples,\n ]\n }\n\n itemSamples = typeMap.array(schema, { sample: itemSamples })\n if (xml.wrapped) {\n res[displayName] = itemSamples\n if (!isEmpty(_attr)) {\n res[displayName].push({ _attr: _attr })\n }\n } else {\n res = itemSamples\n }\n return res\n }\n\n // generate xml sample recursively for object case\n if (type === \"object\") {\n // case literal example\n if (typeof sample === \"string\") {\n return sample\n }\n for (const propName in sample) {\n if (!Object.hasOwn(sample, propName)) {\n continue\n }\n if (props[propName]?.readOnly && !includeReadOnly) {\n continue\n }\n if (props[propName]?.writeOnly && !includeWriteOnly) {\n continue\n }\n if (props[propName]?.xml?.attribute) {\n _attr[props[propName].xml.name || propName] = sample[propName]\n continue\n }\n addPropertyToResult(propName, sample[propName])\n }\n if (!isEmpty(_attr)) {\n res[displayName].push({ _attr: _attr })\n }\n\n return res\n }\n\n res[displayName] = !isEmpty(_attr) ? [{ _attr: _attr }, sample] : sample\n return res\n }\n\n // use schema to generate sample\n if (type === \"array\") {\n let sampleArray = []\n\n if (isJSONSchemaObject(contains)) {\n if (respectXML) {\n contains.xml = contains.xml || schema.xml || {}\n contains.xml.name = contains.xml.name || xml.name\n }\n\n if (Array.isArray(contains.anyOf)) {\n sampleArray.push(\n ...contains.anyOf.map((anyOfSchema) =>\n sampleFromSchemaGeneric(\n merge(anyOfSchema, contains, config),\n config,\n undefined,\n respectXML\n )\n )\n )\n } else if (Array.isArray(contains.oneOf)) {\n sampleArray.push(\n ...contains.oneOf.map((oneOfSchema) =>\n sampleFromSchemaGeneric(\n merge(oneOfSchema, contains, config),\n config,\n undefined,\n respectXML\n )\n )\n )\n } else if (!respectXML || (respectXML && xml.wrapped)) {\n sampleArray.push(\n sampleFromSchemaGeneric(contains, config, undefined, respectXML)\n )\n } else {\n return sampleFromSchemaGeneric(contains, config, undefined, respectXML)\n }\n }\n\n if (isJSONSchemaObject(items)) {\n if (respectXML) {\n items.xml = items.xml || schema.xml || {}\n items.xml.name = items.xml.name || xml.name\n }\n\n if (Array.isArray(items.anyOf)) {\n sampleArray.push(\n ...items.anyOf.map((i) =>\n sampleFromSchemaGeneric(\n merge(i, items, config),\n config,\n undefined,\n respectXML\n )\n )\n )\n } else if (Array.isArray(items.oneOf)) {\n sampleArray.push(\n ...items.oneOf.map((i) =>\n sampleFromSchemaGeneric(\n merge(i, items, config),\n config,\n undefined,\n respectXML\n )\n )\n )\n } else if (!respectXML || (respectXML && xml.wrapped)) {\n sampleArray.push(\n sampleFromSchemaGeneric(items, config, undefined, respectXML)\n )\n } else {\n return sampleFromSchemaGeneric(items, config, undefined, respectXML)\n }\n }\n\n sampleArray = typeMap.array(schema, { sample: sampleArray })\n if (respectXML && xml.wrapped) {\n res[displayName] = sampleArray\n if (!isEmpty(_attr)) {\n res[displayName].push({ _attr: _attr })\n }\n return res\n }\n\n return sampleArray\n }\n\n if (type === \"object\") {\n for (let propName in props) {\n if (!Object.hasOwn(props, propName)) {\n continue\n }\n if (props[propName]?.deprecated) {\n continue\n }\n if (props[propName]?.readOnly && !includeReadOnly) {\n continue\n }\n if (props[propName]?.writeOnly && !includeWriteOnly) {\n continue\n }\n addPropertyToResult(propName)\n }\n if (respectXML && _attr) {\n res[displayName].push({ _attr: _attr })\n }\n\n if (hasExceededMaxProperties()) {\n return res\n }\n\n if (isBooleanJSONSchema(additionalProperties)) {\n if (respectXML) {\n res[displayName].push({ additionalProp: \"Anything can be here\" })\n } else {\n res.additionalProp1 = {}\n }\n propertyAddedCounter++\n } else if (isJSONSchemaObject(additionalProperties)) {\n const additionalProps = additionalProperties\n const additionalPropSample = sampleFromSchemaGeneric(\n additionalProps,\n config,\n undefined,\n respectXML\n )\n\n if (\n respectXML &&\n typeof additionalProps?.xml?.name === \"string\" &&\n additionalProps?.xml?.name !== \"notagname\"\n ) {\n res[displayName].push(additionalPropSample)\n } else {\n const toGenerateCount =\n Number.isInteger(schema.minProperties) &&\n schema.minProperties > 0 &&\n propertyAddedCounter < schema.minProperties\n ? schema.minProperties - propertyAddedCounter\n : 3\n for (let i = 1; i <= toGenerateCount; i++) {\n if (hasExceededMaxProperties()) {\n return res\n }\n if (respectXML) {\n const temp = {}\n temp[\"additionalProp\" + i] = additionalPropSample[\"notagname\"]\n res[displayName].push(temp)\n } else {\n res[\"additionalProp\" + i] = additionalPropSample\n }\n propertyAddedCounter++\n }\n }\n }\n return res\n }\n\n let value\n if (typeof schema.const !== \"undefined\") {\n // display const value\n value = schema.const\n } else if (schema && Array.isArray(schema.enum)) {\n //display enum first value\n value = randomPick(normalizeArray(schema.enum))\n } else {\n // display schema default\n const contentSample = isJSONSchemaObject(schema.contentSchema)\n ? sampleFromSchemaGeneric(\n schema.contentSchema,\n config,\n undefined,\n respectXML\n )\n : undefined\n value = typeMap[type](schema, { sample: contentSample })\n }\n\n if (respectXML) {\n res[displayName] = !isEmpty(_attr) ? [{ _attr: _attr }, value] : value\n return res\n }\n\n return value\n}\n\nexport const createXMLExample = (schema, config, o) => {\n const json = sampleFromSchemaGeneric(schema, config, o, true)\n if (!json) {\n return\n }\n if (typeof json === \"string\") {\n return json\n }\n return XML(json, { declaration: true, indent: \"\\t\" })\n}\n\nexport const sampleFromSchema = (schema, config, o) => {\n return sampleFromSchemaGeneric(schema, config, o, false)\n}\n\nconst resolver = (arg1, arg2, arg3) => [\n arg1,\n JSON.stringify(arg2),\n JSON.stringify(arg3),\n]\n\nexport const memoizedCreateXMLExample = memoizeN(createXMLExample, resolver)\n\nexport const memoizedSampleFromSchema = memoizeN(sampleFromSchema, resolver)\n","/**\n * @prettier\n */\n\nexport const applyArrayConstraints = (array, constraints = {}) => {\n const { minItems, maxItems, uniqueItems } = constraints\n const { contains, minContains, maxContains } = constraints\n let constrainedArray = [...array]\n\n if (contains != null && typeof contains === \"object\") {\n if (Number.isInteger(minContains) && minContains > 1) {\n const containsItem = constrainedArray.at(0)\n for (let i = 1; i < minContains; i += 1) {\n constrainedArray.unshift(containsItem)\n }\n }\n if (Number.isInteger(maxContains) && maxContains > 0) {\n /**\n * This is noop. `minContains` already generate minimum required\n * number of items that satisfies `contains`. `maxContains` would\n * have no effect.\n */\n }\n }\n\n if (Number.isInteger(maxItems) && maxItems > 0) {\n constrainedArray = array.slice(0, maxItems)\n }\n if (Number.isInteger(minItems) && minItems > 0) {\n for (let i = 0; constrainedArray.length < minItems; i += 1) {\n constrainedArray.push(constrainedArray[i % constrainedArray.length])\n }\n }\n\n if (uniqueItems === true) {\n /**\n * If uniqueItems is true, it implies that every item in the array must be unique.\n * This overrides any minItems constraint that cannot be satisfied with unique items.\n * So if minItems is greater than the number of unique items,\n * it should be reduced to the number of unique items.\n */\n constrainedArray = Array.from(new Set(constrainedArray))\n }\n\n return constrainedArray\n}\n\nconst arrayType = (schema, { sample }) => {\n return applyArrayConstraints(sample, schema)\n}\n\nexport default arrayType\n","/**\n * @prettier\n */\n\nconst booleanType = (schema) => {\n return typeof schema.default === \"boolean\" ? schema.default : true\n}\n\nexport default booleanType\n","/**\n * @prettier\n */\nimport arrayType from \"./array\"\nimport objectType from \"./object\"\nimport stringType from \"./string\"\nimport numberType from \"./number\"\nimport integerType from \"./integer\"\nimport booleanType from \"./boolean\"\nimport nullType from \"./null\"\n\nconst typeMap = {\n array: arrayType,\n object: objectType,\n string: stringType,\n number: numberType,\n integer: integerType,\n boolean: booleanType,\n null: nullType,\n}\n\nexport default new Proxy(typeMap, {\n get(target, prop) {\n if (typeof prop === \"string\" && Object.hasOwn(target, prop)) {\n return target[prop]\n }\n\n return () => `Unknown Type: ${prop}`\n },\n})\n","/**\n * @prettier\n */\nimport { integer as randomInteger } from \"../core/random\"\nimport formatAPI from \"../api/formatAPI\"\nimport int32Generator from \"../generators/int32\"\nimport int64Generator from \"../generators/int64\"\n\nconst generateFormat = (schema) => {\n const { format } = schema\n\n const formatGenerator = formatAPI(format)\n if (typeof formatGenerator === \"function\") {\n return formatGenerator(schema)\n }\n\n switch (format) {\n case \"int32\": {\n return int32Generator()\n }\n case \"int64\": {\n return int64Generator()\n }\n }\n\n return randomInteger()\n}\nconst integerType = (schema) => {\n const { format } = schema\n\n if (typeof format === \"string\") {\n return generateFormat(schema)\n }\n\n return randomInteger()\n}\n\nexport default integerType\n","/**\n * @prettier\n */\n\nconst nullType = () => {\n return null\n}\n\nexport default nullType\n","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_number_epsilon_c9a81fe6__[\"default\"] });","/**\n * @prettier\n */\nimport { number as randomNumber } from \"../core/random\"\nimport formatAPI from \"../api/formatAPI\"\nimport floatGenerator from \"../generators/float\"\nimport doubleGenerator from \"../generators/double\"\n\nconst generateFormat = (schema) => {\n const { format } = schema\n\n const formatGenerator = formatAPI(format)\n if (typeof formatGenerator === \"function\") {\n return formatGenerator(schema)\n }\n\n switch (format) {\n case \"float\": {\n return floatGenerator()\n }\n case \"double\": {\n return doubleGenerator()\n }\n }\n\n return randomNumber()\n}\n\nconst applyNumberConstraints = (number, constraints = {}) => {\n const { minimum, maximum, exclusiveMinimum, exclusiveMaximum } = constraints\n const { multipleOf } = constraints\n const epsilon = Number.isInteger(number) ? 1 : Number.EPSILON\n let minValue = typeof minimum === \"number\" ? minimum : null\n let maxValue = typeof maximum === \"number\" ? maximum : null\n let constrainedNumber = number\n\n if (typeof exclusiveMinimum === \"number\") {\n minValue =\n minValue !== null\n ? Math.max(minValue, exclusiveMinimum + epsilon)\n : exclusiveMinimum + epsilon\n }\n if (typeof exclusiveMaximum === \"number\") {\n maxValue =\n maxValue !== null\n ? Math.min(maxValue, exclusiveMaximum - epsilon)\n : exclusiveMaximum - epsilon\n }\n constrainedNumber =\n (minValue > maxValue && number) || minValue || maxValue || constrainedNumber\n\n if (typeof multipleOf === \"number\" && multipleOf > 0) {\n const remainder = constrainedNumber % multipleOf\n constrainedNumber =\n remainder === 0\n ? constrainedNumber\n : constrainedNumber + multipleOf - remainder\n }\n\n return constrainedNumber\n}\n\nconst numberType = (schema) => {\n const { format } = schema\n let generatedNumber\n\n if (typeof format === \"string\") {\n generatedNumber = generateFormat(schema)\n } else {\n generatedNumber = randomNumber()\n }\n\n return applyNumberConstraints(generatedNumber, schema)\n}\n\nexport default numberType\n","/**\n * @prettier\n */\n\nconst objectType = () => {\n throw new Error(\"Not implemented\")\n}\n\nexport default objectType\n","/**\n * @prettier\n */\nimport identity from \"lodash/identity\"\n\nimport { string as randomString, randexp } from \"../core/random\"\nimport { isJSONSchema } from \"../core/predicates\"\nimport emailGenerator from \"../generators/email\"\nimport idnEmailGenerator from \"../generators/idn-email\"\nimport hostnameGenerator from \"../generators/hostname\"\nimport idnHostnameGenerator from \"../generators/idn-hostname\"\nimport ipv4Generator from \"../generators/ipv4\"\nimport ipv6Generator from \"../generators/ipv6\"\nimport uriGenerator from \"../generators/uri\"\nimport uriReferenceGenerator from \"../generators/uri-reference\"\nimport iriGenerator from \"../generators/iri\"\nimport iriReferenceGenerator from \"../generators/iri-reference\"\nimport uuidGenerator from \"../generators/uuid\"\nimport uriTemplateGenerator from \"../generators/uri-template\"\nimport jsonPointerGenerator from \"../generators/json-pointer\"\nimport relativeJsonPointerGenerator from \"../generators/relative-json-pointer\"\nimport dateTimeGenerator from \"../generators/date-time\"\nimport dateGenerator from \"../generators/date\"\nimport timeGenerator from \"../generators/time\"\nimport durationGenerator from \"../generators/duration\"\nimport passwordGenerator from \"../generators/password\"\nimport regexGenerator from \"../generators/regex\"\nimport formatAPI from \"../api/formatAPI\"\nimport encoderAPI from \"../api/encoderAPI\"\nimport mediaTypeAPI from \"../api/mediaTypeAPI\"\n\nconst generateFormat = (schema) => {\n const { format } = schema\n\n const formatGenerator = formatAPI(format)\n if (typeof formatGenerator === \"function\") {\n return formatGenerator(schema)\n }\n\n switch (format) {\n case \"email\": {\n return emailGenerator()\n }\n case \"idn-email\": {\n return idnEmailGenerator()\n }\n case \"hostname\": {\n return hostnameGenerator()\n }\n case \"idn-hostname\": {\n return idnHostnameGenerator()\n }\n case \"ipv4\": {\n return ipv4Generator()\n }\n case \"ipv6\": {\n return ipv6Generator()\n }\n case \"uri\": {\n return uriGenerator()\n }\n case \"uri-reference\": {\n return uriReferenceGenerator()\n }\n case \"iri\": {\n return iriGenerator()\n }\n case \"iri-reference\": {\n return iriReferenceGenerator()\n }\n case \"uuid\": {\n return uuidGenerator()\n }\n case \"uri-template\": {\n return uriTemplateGenerator()\n }\n case \"json-pointer\": {\n return jsonPointerGenerator()\n }\n case \"relative-json-pointer\": {\n return relativeJsonPointerGenerator()\n }\n case \"date-time\": {\n return dateTimeGenerator()\n }\n case \"date\": {\n return dateGenerator()\n }\n case \"time\": {\n return timeGenerator()\n }\n case \"duration\": {\n return durationGenerator()\n }\n case \"password\": {\n return passwordGenerator()\n }\n case \"regex\": {\n return regexGenerator()\n }\n }\n\n return randomString()\n}\n\nconst applyStringConstraints = (string, constraints = {}) => {\n const { maxLength, minLength } = constraints\n let constrainedString = string\n\n if (Number.isInteger(maxLength) && maxLength > 0) {\n constrainedString = constrainedString.slice(0, maxLength)\n }\n if (Number.isInteger(minLength) && minLength > 0) {\n let i = 0\n while (constrainedString.length < minLength) {\n constrainedString += constrainedString[i++ % constrainedString.length]\n }\n }\n\n return constrainedString\n}\nconst stringType = (schema, { sample } = {}) => {\n const { contentEncoding, contentMediaType, contentSchema } = schema\n const { pattern, format } = schema\n const encode = encoderAPI(contentEncoding) || identity\n let generatedString\n\n if (typeof pattern === \"string\") {\n generatedString = randexp(pattern)\n } else if (typeof format === \"string\") {\n generatedString = generateFormat(schema)\n } else if (\n isJSONSchema(contentSchema) &&\n typeof contentMediaType === \"string\" &&\n typeof sample !== \"undefined\"\n ) {\n if (Array.isArray(sample) || typeof sample === \"object\") {\n generatedString = JSON.stringify(sample)\n } else {\n generatedString = String(sample)\n }\n } else if (typeof contentMediaType === \"string\") {\n const mediaTypeGenerator = mediaTypeAPI(contentMediaType)\n if (typeof mediaTypeGenerator === \"function\") {\n generatedString = mediaTypeGenerator(schema)\n }\n } else {\n generatedString = randomString()\n }\n\n return encode(applyStringConstraints(generatedString, schema))\n}\n\nexport default stringType\n","import { normalizeArray } from \"core/utils\"\n\nexport const UPDATE_LAYOUT = \"layout_update_layout\"\nexport const UPDATE_FILTER = \"layout_update_filter\"\nexport const UPDATE_MODE = \"layout_update_mode\"\nexport const SHOW = \"layout_show\"\n\n// export const ONLY_SHOW = \"layout_only_show\"\n\nexport function updateLayout(layout) {\n return {\n type: UPDATE_LAYOUT,\n payload: layout\n }\n}\n\nexport function updateFilter(filter) {\n return {\n type: UPDATE_FILTER,\n payload: filter\n }\n}\n\nexport function show(thing, shown=true) {\n thing = normalizeArray(thing)\n return {\n type: SHOW,\n payload: {thing, shown}\n }\n}\n\n// Simple string key-store, used for\nexport function changeMode(thing, mode=\"\") {\n thing = normalizeArray(thing)\n return {\n type: UPDATE_MODE,\n payload: {thing, mode}\n }\n}\n","import reducers from \"./reducers\"\nimport * as actions from \"./actions\"\nimport * as selectors from \"./selectors\"\nimport * as wrapSelectors from \"./spec-extensions/wrap-selector\"\n\nexport default function() {\n return {\n statePlugins: {\n layout: {\n reducers,\n actions,\n selectors\n },\n spec: {\n wrapSelectors\n }\n }\n }\n}\n","import { fromJS } from \"immutable\"\nimport {\n UPDATE_LAYOUT,\n UPDATE_FILTER,\n UPDATE_MODE,\n SHOW\n} from \"./actions\"\n\nexport default {\n\n [UPDATE_LAYOUT]: (state, action) => state.set(\"layout\", action.payload),\n\n [UPDATE_FILTER]: (state, action) => state.set(\"filter\", action.payload),\n\n [SHOW]: (state, action) => {\n const isShown = action.payload.shown\n // This is one way to serialize an array, another (preferred) is to convert to json-pointer\n // TODO: use json-pointer serilization instead of fromJS(...), for performance\n const thingToShow = fromJS(action.payload.thing)\n // This is a map of paths to bools\n // eg: [one, two] => true\n // eg: [one] => false\n return state.update(\"shown\", fromJS({}), a => a.set(thingToShow, isShown))\n },\n\n [UPDATE_MODE]: (state, action) => {\n let thing = action.payload.thing\n let mode = action.payload.mode\n return state.setIn([\"modes\"].concat(thing), (mode || \"\") + \"\")\n }\n\n}\n","import { createSelector } from \"reselect\"\nimport { normalizeArray } from \"core/utils\"\nimport { fromJS } from \"immutable\"\n\nconst state = state => state\n\nexport const current = state => state.get(\"layout\")\n\nexport const currentFilter = state => state.get(\"filter\")\n\nexport const isShown = (state, thing, def) => {\n thing = normalizeArray(thing)\n return state.get(\"shown\", fromJS({})).get(fromJS(thing), def)\n}\n\nexport const whatMode = (state, thing, def=\"\") => {\n thing = normalizeArray(thing)\n return state.getIn([\"modes\", ...thing], def)\n}\n\nexport const showSummary = createSelector(\n state,\n state => !isShown(state, \"editor\")\n)\n","\nexport const taggedOperations = (oriSelector, system) => (state, ...args) => {\n let taggedOps = oriSelector(state, ...args)\n\n const { fn, layoutSelectors, getConfigs } = system.getSystem()\n const configs = getConfigs()\n const { maxDisplayedTags } = configs\n\n // Filter, if requested\n let filter = layoutSelectors.currentFilter()\n if (filter) {\n if (filter !== true && filter !== \"true\" && filter !== \"false\") {\n taggedOps = fn.opsFilter(taggedOps, filter)\n }\n }\n // Limit to [max] items, if specified\n if (maxDisplayedTags && !isNaN(maxDisplayedTags) && maxDisplayedTags >= 0) {\n taggedOps = taggedOps.slice(0, maxDisplayedTags)\n }\n\n return taggedOps\n}\n","export default function ({configs}) {\n\n const levels = {\n \"debug\": 0,\n \"info\": 1,\n \"log\": 2,\n \"warn\": 3,\n \"error\": 4\n }\n\n const getLevel = (level) => levels[level] || -1\n\n let { logLevel } = configs\n let logLevelInt = getLevel(logLevel)\n\n function log(level, ...args) {\n if(getLevel(level) >= logLevelInt)\n // eslint-disable-next-line no-console\n console[level](...args)\n }\n\n log.warn = log.bind(null, \"warn\")\n log.error = log.bind(null, \"error\")\n log.info = log.bind(null, \"info\")\n log.debug = log.bind(null, \"debug\")\n\n return { rootInjects: { log } }\n}\n","// Actions conform to FSA (flux-standard-actions)\n// {type: string,payload: Any|Error, meta: obj, error: bool}\n\nexport const UPDATE_SELECTED_SERVER = \"oas3_set_servers\"\nexport const UPDATE_REQUEST_BODY_VALUE = \"oas3_set_request_body_value\"\nexport const UPDATE_REQUEST_BODY_VALUE_RETAIN_FLAG = \"oas3_set_request_body_retain_flag\"\nexport const UPDATE_REQUEST_BODY_INCLUSION = \"oas3_set_request_body_inclusion\"\nexport const UPDATE_ACTIVE_EXAMPLES_MEMBER = \"oas3_set_active_examples_member\"\nexport const UPDATE_REQUEST_CONTENT_TYPE = \"oas3_set_request_content_type\"\nexport const UPDATE_RESPONSE_CONTENT_TYPE = \"oas3_set_response_content_type\"\nexport const UPDATE_SERVER_VARIABLE_VALUE = \"oas3_set_server_variable_value\"\nexport const SET_REQUEST_BODY_VALIDATE_ERROR = \"oas3_set_request_body_validate_error\"\nexport const CLEAR_REQUEST_BODY_VALIDATE_ERROR = \"oas3_clear_request_body_validate_error\"\nexport const CLEAR_REQUEST_BODY_VALUE = \"oas3_clear_request_body_value\"\n\nexport function setSelectedServer (selectedServerUrl, namespace) {\n return {\n type: UPDATE_SELECTED_SERVER,\n payload: {selectedServerUrl, namespace}\n }\n}\n\nexport function setRequestBodyValue ({ value, pathMethod }) {\n return {\n type: UPDATE_REQUEST_BODY_VALUE,\n payload: { value, pathMethod }\n }\n}\n\nexport const setRetainRequestBodyValueFlag = ({ value, pathMethod }) => {\n return {\n type: UPDATE_REQUEST_BODY_VALUE_RETAIN_FLAG,\n payload: { value, pathMethod }\n }\n}\n\n\nexport function setRequestBodyInclusion ({ value, pathMethod, name }) {\n return {\n type: UPDATE_REQUEST_BODY_INCLUSION,\n payload: { value, pathMethod, name }\n }\n}\n\nexport function setActiveExamplesMember ({ name, pathMethod, contextType, contextName }) {\n return {\n type: UPDATE_ACTIVE_EXAMPLES_MEMBER,\n payload: { name, pathMethod, contextType, contextName }\n }\n}\n\nexport function setRequestContentType ({ value, pathMethod }) {\n return {\n type: UPDATE_REQUEST_CONTENT_TYPE,\n payload: { value, pathMethod }\n }\n}\n\nexport function setResponseContentType ({ value, path, method }) {\n return {\n type: UPDATE_RESPONSE_CONTENT_TYPE,\n payload: { value, path, method }\n }\n}\n\nexport function setServerVariableValue ({ server, namespace, key, val }) {\n return {\n type: UPDATE_SERVER_VARIABLE_VALUE,\n payload: { server, namespace, key, val }\n }\n}\n\nexport const setRequestBodyValidateError = ({ path, method, validationErrors }) => {\n return {\n type: SET_REQUEST_BODY_VALIDATE_ERROR,\n payload: { path, method, validationErrors }\n }\n}\n\nexport const clearRequestBodyValidateError = ({ path, method }) => {\n return {\n type: CLEAR_REQUEST_BODY_VALIDATE_ERROR,\n payload: { path, method }\n }\n}\n\nexport const initRequestBodyValidateError = ({ pathMethod } ) => {\n return {\n type: CLEAR_REQUEST_BODY_VALIDATE_ERROR,\n payload: { path: pathMethod[0], method: pathMethod[1] }\n }\n}\n\nexport const clearRequestBodyValue = ({ pathMethod }) => {\n return {\n type: CLEAR_REQUEST_BODY_VALUE,\n payload: { pathMethod }\n }\n}\n","import { createSelector } from \"reselect\"\nimport { List, Map, fromJS } from \"immutable\"\n\n\n// Helpers\n\nconst state = state => state\n\nfunction onlyOAS3(selector) {\n return (ori, system) => (...args) => {\n if(system.getSystem().specSelectors.isOAS3()) {\n // Pass the spec plugin state to Reselect to trigger on securityDefinitions update\n let resolvedSchemes = system.getState().getIn([\"spec\", \"resolvedSubtrees\",\n \"components\", \"securitySchemes\"])\n return selector(system, resolvedSchemes, ...args)\n } else {\n return ori(...args)\n }\n }\n}\n\nexport const definitionsToAuthorize = onlyOAS3(createSelector(\n state,\n ({specSelectors}) => specSelectors.securityDefinitions(),\n (system, definitions) => {\n // Coerce our OpenAPI 3.0 definitions into monoflow definitions\n // that look like Swagger2 definitions.\n let list = List()\n\n if(!definitions) {\n return list\n }\n\n definitions.entrySeq().forEach( ([ defName, definition ]) => {\n const type = definition.get(\"type\")\n\n if(type === \"oauth2\") {\n definition.get(\"flows\").entrySeq().forEach(([flowKey, flowVal]) => {\n let translatedDef = fromJS({\n flow: flowKey,\n authorizationUrl: flowVal.get(\"authorizationUrl\"),\n tokenUrl: flowVal.get(\"tokenUrl\"),\n scopes: flowVal.get(\"scopes\"),\n type: definition.get(\"type\"),\n description: definition.get(\"description\")\n })\n\n list = list.push(new Map({\n [defName]: translatedDef.filter((v) => {\n // filter out unset values, sometimes `authorizationUrl`\n // and `tokenUrl` come out as `undefined` in the data\n return v !== undefined\n })\n }))\n })\n }\n if(type === \"http\" || type === \"apiKey\") {\n list = list.push(new Map({\n [defName]: definition\n }))\n }\n if(type === \"openIdConnect\" && definition.get(\"openIdConnectData\")) {\n let oidcData = definition.get(\"openIdConnectData\")\n let grants = oidcData.get(\"grant_types_supported\") || [\"authorization_code\", \"implicit\"]\n grants.forEach((grant) => {\n // Convert from OIDC list of scopes to the OAS-style map with empty descriptions\n let translatedScopes = oidcData.get(\"scopes_supported\") &&\n oidcData.get(\"scopes_supported\").reduce((acc, cur) => acc.set(cur, \"\"), new Map())\n\n let translatedDef = fromJS({\n flow: grant,\n authorizationUrl: oidcData.get(\"authorization_endpoint\"),\n tokenUrl: oidcData.get(\"token_endpoint\"),\n scopes: translatedScopes,\n type: \"oauth2\",\n openIdConnectUrl: definition.get(\"openIdConnectUrl\")\n })\n\n list = list.push(new Map({\n [defName]: translatedDef.filter((v) => {\n // filter out unset values, sometimes `authorizationUrl`\n // and `tokenUrl` come out as `undefined` in the data\n return v !== undefined\n })\n }))\n })\n }\n })\n\n return list\n }\n))\n","/**\n * @prettier\n */\nimport React from \"react\"\nimport PropTypes from \"prop-types\"\nimport ImPropTypes from \"react-immutable-proptypes\"\n\nconst Callbacks = ({ callbacks, specPath, specSelectors, getComponent }) => {\n const operationDTOs = specSelectors.callbacksOperations({\n callbacks,\n specPath,\n })\n const callbackNames = Object.keys(operationDTOs)\n\n const OperationContainer = getComponent(\"OperationContainer\", true)\n\n if (callbackNames.length === 0) return No callbacks\n\n return (\n
    \n {callbackNames.map((callbackName) => (\n
    \n

    {callbackName}

    \n\n {operationDTOs[callbackName].map((operationDTO) => (\n \n ))}\n
    \n ))}\n
    \n )\n}\n\nCallbacks.propTypes = {\n getComponent: PropTypes.func.isRequired,\n specSelectors: PropTypes.shape({\n callbacksOperations: PropTypes.func.isRequired,\n }).isRequired,\n callbacks: ImPropTypes.iterable.isRequired,\n specPath: ImPropTypes.list.isRequired,\n}\n\nexport default Callbacks\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\n\nexport default class HttpAuth extends React.Component {\n static propTypes = {\n authorized: PropTypes.object,\n getComponent: PropTypes.func.isRequired,\n errSelectors: PropTypes.object.isRequired,\n schema: PropTypes.object.isRequired,\n name: PropTypes.string.isRequired,\n onChange: PropTypes.func\n }\n\n constructor(props, context) {\n super(props, context)\n let { name, schema } = this.props\n let value = this.getValue()\n\n this.state = {\n name: name,\n schema: schema,\n value: value\n }\n }\n\n getValue () {\n let { name, authorized } = this.props\n\n return authorized && authorized.getIn([name, \"value\"])\n }\n\n onChange =(e) => {\n let { onChange } = this.props\n let { value, name } = e.target\n\n let newValue = Object.assign({}, this.state.value)\n\n if(name) {\n newValue[name] = value\n } else {\n newValue = value\n }\n\n this.setState({ value: newValue }, () => onChange(this.state))\n\n }\n\n render() {\n let { schema, getComponent, errSelectors, name } = this.props\n const Input = getComponent(\"Input\")\n const Row = getComponent(\"Row\")\n const Col = getComponent(\"Col\")\n const AuthError = getComponent(\"authError\")\n const Markdown = getComponent(\"Markdown\", true)\n const JumpToPath = getComponent(\"JumpToPath\", true)\n\n const scheme = (schema.get(\"scheme\") || \"\").toLowerCase()\n let value = this.getValue()\n let errors = errSelectors.allErrors().filter( err => err.get(\"authId\") === name)\n\n if(scheme === \"basic\") {\n let username = value ? value.get(\"username\") : null\n return
    \n

    \n { name || schema.get(\"name\") } \n (http, Basic)\n \n

    \n { username &&
    Authorized
    }\n \n \n \n \n \n {\n username ? { username } \n : \n }\n \n \n \n {\n username ? ****** \n : \n }\n \n {\n errors.valueSeq().map( (error, key) => {\n return \n } )\n }\n
    \n }\n\n if(scheme === \"bearer\") {\n return (\n
    \n

    \n { name || schema.get(\"name\") } \n (http, Bearer)\n \n

    \n { value &&
    Authorized
    }\n \n \n \n \n \n {\n value ? ****** \n : \n }\n \n {\n errors.valueSeq().map( (error, key) => {\n return \n } )\n }\n
    \n )\n }\n return
    \n {name} HTTP authentication: unsupported scheme {`'${scheme}'`}\n
    \n }\n}\n","import Callbacks from \"./callbacks\"\nimport RequestBody from \"./request-body\"\nimport OperationLink from \"./operation-link\"\nimport Servers from \"./servers\"\nimport ServersContainer from \"./servers-container\"\nimport RequestBodyEditor from \"./request-body-editor\"\nimport HttpAuth from \"./http-auth\"\nimport OperationServers from \"./operation-servers\"\n\nexport default {\n Callbacks,\n HttpAuth,\n RequestBody,\n Servers,\n ServersContainer,\n RequestBodyEditor,\n OperationServers,\n operationLink: OperationLink,\n}\n","import React, { Component } from \"react\"\nimport PropTypes from \"prop-types\"\nimport ImPropTypes from \"react-immutable-proptypes\"\n\nclass OperationLink extends Component {\n render() {\n const { link, name, getComponent } = this.props\n\n const Markdown = getComponent(\"Markdown\", true)\n\n let targetOp = link.get(\"operationId\") || link.get(\"operationRef\")\n let parameters = link.get(\"parameters\") && link.get(\"parameters\").toJS()\n let description = link.get(\"description\")\n\n return
    \n
    \n {name}\n { description ? : null }\n
    \n
    \n        Operation `{targetOp}`

    \n Parameters {padString(0, JSON.stringify(parameters, null, 2)) || \"{}\"}
    \n
    \n
    \n }\n\n}\n\nfunction padString(n, string) {\n if(typeof string !== \"string\") { return \"\" }\n return string\n .split(\"\\n\")\n .map((line, i) => i > 0 ? Array(n + 1).join(\" \") + line : line)\n .join(\"\\n\")\n}\n\nOperationLink.propTypes = {\n getComponent: PropTypes.func.isRequired,\n link: ImPropTypes.orderedMap.isRequired,\n name: PropTypes.String\n}\n\nexport default OperationLink\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\nimport ImPropTypes from \"react-immutable-proptypes\"\n\nexport default class OperationServers extends React.Component {\n static propTypes = {\n // for self\n path: PropTypes.string.isRequired,\n method: PropTypes.string.isRequired,\n operationServers: ImPropTypes.list,\n pathServers: ImPropTypes.list,\n setSelectedServer: PropTypes.func.isRequired,\n setServerVariableValue: PropTypes.func.isRequired,\n getSelectedServer: PropTypes.func.isRequired,\n getServerVariable: PropTypes.func.isRequired,\n getEffectiveServerValue: PropTypes.func.isRequired,\n\n // utils\n getComponent: PropTypes.func.isRequired\n }\n\n setSelectedServer = (server) => {\n const { path, method } = this.props\n // FIXME: we should be keeping up with this in props/state upstream of us\n // instead of cheatingâ„¢ with `forceUpdate`\n this.forceUpdate()\n return this.props.setSelectedServer(server, `${path}:${method}`)\n }\n\n setServerVariableValue = (obj) => {\n const { path, method } = this.props\n // FIXME: we should be keeping up with this in props/state upstream of us\n // instead of cheatingâ„¢ with `forceUpdate`\n this.forceUpdate()\n return this.props.setServerVariableValue({\n ...obj,\n namespace: `${path}:${method}`\n })\n }\n\n getSelectedServer = () => {\n const { path, method } = this.props\n return this.props.getSelectedServer(`${path}:${method}`)\n }\n\n getServerVariable = (server, key) => {\n const { path, method } = this.props\n return this.props.getServerVariable({\n namespace: `${path}:${method}`,\n server\n }, key)\n }\n\n getEffectiveServerValue = (server) => {\n const { path, method } = this.props\n return this.props.getEffectiveServerValue({\n server,\n namespace: `${path}:${method}`\n })\n }\n\n render() {\n const {\n // for self\n operationServers,\n pathServers,\n\n // util\n getComponent\n } = this.props\n\n if(!operationServers && !pathServers) {\n return null\n }\n\n const Servers = getComponent(\"Servers\")\n\n const serversToDisplay = operationServers || pathServers\n const displaying = operationServers ? \"operation\" : \"path\"\n\n return
    \n
    \n
    \n

    Servers

    \n
    \n
    \n
    \n

    \n These {displaying}-level options override the global server options.\n

    \n \n
    \n
    \n }\n}\n","import React, { PureComponent } from \"react\"\nimport PropTypes from \"prop-types\"\nimport cx from \"classnames\"\nimport { stringify } from \"core/utils\"\n\nconst NOOP = Function.prototype\n\nexport default class RequestBodyEditor extends PureComponent {\n\n static propTypes = {\n onChange: PropTypes.func,\n getComponent: PropTypes.func.isRequired,\n value: PropTypes.string,\n defaultValue: PropTypes.string,\n errors: PropTypes.array,\n }\n\n static defaultProps = {\n onChange: NOOP,\n userHasEditedBody: false,\n }\n\n constructor(props, context) {\n super(props, context)\n\n this.state = {\n value: stringify(props.value) || props.defaultValue\n }\n\n // this is the glue that makes sure our initial value gets set as the\n // current request body value\n // TODO: achieve this in a selector instead\n props.onChange(props.value)\n }\n\n applyDefaultValue = (nextProps) => {\n const { onChange, defaultValue } = (nextProps ? nextProps : this.props)\n\n this.setState({\n value: defaultValue\n })\n\n return onChange(defaultValue)\n }\n\n onChange = (value) => {\n this.props.onChange(stringify(value))\n }\n\n onDomChange = e => {\n const inputValue = e.target.value\n\n this.setState({\n value: inputValue,\n }, () => this.onChange(inputValue))\n }\n\n UNSAFE_componentWillReceiveProps(nextProps) {\n if(\n this.props.value !== nextProps.value &&\n nextProps.value !== this.state.value\n ) {\n\n this.setState({\n value: stringify(nextProps.value)\n })\n }\n\n\n\n if(!nextProps.value && nextProps.defaultValue && !!this.state.value) {\n // if new value is falsy, we have a default, AND the falsy value didn't\n // come from us originally\n this.applyDefaultValue(nextProps)\n }\n }\n\n render() {\n let {\n getComponent,\n errors,\n } = this.props\n\n let {\n value\n } = this.state\n\n let isInvalid = errors.size > 0 ? true : false\n const TextArea = getComponent(\"TextArea\")\n\n return (\n
    \n \n
    \n )\n\n }\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\nimport ImPropTypes from \"react-immutable-proptypes\"\nimport { Map, OrderedMap, List } from \"immutable\"\nimport { getCommonExtensions, stringify, isEmptyValue } from \"core/utils\"\nimport { getKnownSyntaxHighlighterLanguage } from \"core/utils/jsonParse\"\n\nexport const getDefaultRequestBodyValue = (requestBody, mediaType, activeExamplesKey, fn) => {\n const mediaTypeValue = requestBody.getIn([\"content\", mediaType])\n const schema = mediaTypeValue.get(\"schema\").toJS()\n\n const hasExamplesKey = mediaTypeValue.get(\"examples\") !== undefined\n const exampleSchema = mediaTypeValue.get(\"example\")\n const mediaTypeExample = hasExamplesKey\n ? mediaTypeValue.getIn([\n \"examples\",\n activeExamplesKey,\n \"value\"\n ])\n : exampleSchema\n\n const exampleValue = fn.getSampleSchema(\n schema,\n mediaType,\n {\n includeWriteOnly: true\n },\n mediaTypeExample\n )\n return stringify(exampleValue)\n}\n\n\n\nconst RequestBody = ({\n userHasEditedBody,\n requestBody,\n requestBodyValue,\n requestBodyInclusionSetting,\n requestBodyErrors,\n getComponent,\n getConfigs,\n specSelectors,\n fn,\n contentType,\n isExecute,\n specPath,\n onChange,\n onChangeIncludeEmpty,\n activeExamplesKey,\n updateActiveExamplesKey,\n setRetainRequestBodyValueFlag\n}) => {\n const handleFile = (e) => {\n onChange(e.target.files[0])\n }\n const setIsIncludedOptions = (key) => {\n let options = {\n key,\n shouldDispatchInit: false,\n defaultValue: true\n }\n let currentInclusion = requestBodyInclusionSetting.get(key, \"no value\")\n if (currentInclusion === \"no value\") {\n options.shouldDispatchInit = true\n // future: can get/set defaultValue from a config setting\n }\n return options\n }\n\n const Markdown = getComponent(\"Markdown\", true)\n const ModelExample = getComponent(\"modelExample\")\n const RequestBodyEditor = getComponent(\"RequestBodyEditor\")\n const HighlightCode = getComponent(\"highlightCode\")\n const ExamplesSelectValueRetainer = getComponent(\"ExamplesSelectValueRetainer\")\n const Example = getComponent(\"Example\")\n const ParameterIncludeEmpty = getComponent(\"ParameterIncludeEmpty\")\n\n const { showCommonExtensions } = getConfigs()\n\n const requestBodyDescription = (requestBody && requestBody.get(\"description\")) || null\n const requestBodyContent = (requestBody && requestBody.get(\"content\")) || new OrderedMap()\n contentType = contentType || requestBodyContent.keySeq().first() || \"\"\n\n const mediaTypeValue = requestBodyContent.get(contentType, OrderedMap())\n const schemaForMediaType = mediaTypeValue.get(\"schema\", OrderedMap())\n const rawExamplesOfMediaType = mediaTypeValue.get(\"examples\", null)\n const sampleForMediaType = rawExamplesOfMediaType?.map((container, key) => {\n const val = container?.get(\"value\", null)\n if(val) {\n container = container.set(\"value\", getDefaultRequestBodyValue(\n requestBody,\n contentType,\n key,\n fn,\n ), val)\n }\n return container\n })\n\n const handleExamplesSelect = (key /*, { isSyntheticChange } */) => {\n updateActiveExamplesKey(key)\n }\n requestBodyErrors = List.isList(requestBodyErrors) ? requestBodyErrors : List()\n\n if(!mediaTypeValue.size) {\n return null\n }\n\n const isObjectContent = mediaTypeValue.getIn([\"schema\", \"type\"]) === \"object\"\n const isBinaryFormat = mediaTypeValue.getIn([\"schema\", \"format\"]) === \"binary\"\n const isBase64Format = mediaTypeValue.getIn([\"schema\", \"format\"]) === \"base64\"\n\n if(\n contentType === \"application/octet-stream\"\n || contentType.indexOf(\"image/\") === 0\n || contentType.indexOf(\"audio/\") === 0\n || contentType.indexOf(\"video/\") === 0\n || isBinaryFormat\n || isBase64Format\n ) {\n const Input = getComponent(\"Input\")\n\n if(!isExecute) {\n return \n Example values are not available for {contentType} media types.\n \n }\n\n return \n }\n\n if (\n isObjectContent &&\n (\n contentType === \"application/x-www-form-urlencoded\" ||\n contentType.indexOf(\"multipart/\") === 0\n ) &&\n schemaForMediaType.get(\"properties\", OrderedMap()).size > 0\n ) {\n const JsonSchemaForm = getComponent(\"JsonSchemaForm\")\n const ParameterExt = getComponent(\"ParameterExt\")\n const bodyProperties = schemaForMediaType.get(\"properties\", OrderedMap())\n requestBodyValue = Map.isMap(requestBodyValue) ? requestBodyValue : OrderedMap()\n\n return
    \n { requestBodyDescription &&\n \n }\n \n \n {\n Map.isMap(bodyProperties) && bodyProperties.entrySeq().map(([key, prop]) => {\n if (prop.get(\"readOnly\")) return\n\n let commonExt = showCommonExtensions ? getCommonExtensions(prop) : null\n const required = schemaForMediaType.get(\"required\", List()).includes(key)\n const type = prop.get(\"type\")\n const format = prop.get(\"format\")\n const description = prop.get(\"description\")\n const currentValue = requestBodyValue.getIn([key, \"value\"])\n const currentErrors = requestBodyValue.getIn([key, \"errors\"]) || requestBodyErrors\n const included = requestBodyInclusionSetting.get(key) || false\n\n const useInitialValFromSchemaSamples = prop.has(\"default\")\n || prop.has(\"example\")\n || prop.hasIn([\"items\", \"example\"])\n || prop.hasIn([\"items\", \"default\"])\n const useInitialValFromEnum = prop.has(\"enum\") && (prop.get(\"enum\").size === 1 || required)\n const useInitialValue = useInitialValFromSchemaSamples || useInitialValFromEnum\n\n let initialValue = \"\"\n if (type === \"array\" && !useInitialValue) {\n initialValue = []\n }\n if (type === \"object\" || useInitialValue) {\n // TODO: what about example or examples from requestBody could be passed as exampleOverride\n initialValue = fn.getSampleSchema(prop, false, {\n includeWriteOnly: true\n })\n }\n\n if (typeof initialValue !== \"string\" && type === \"object\") {\n initialValue = stringify(initialValue)\n }\n if (typeof initialValue === \"string\" && type === \"array\") {\n initialValue = JSON.parse(initialValue)\n }\n\n const isFile = type === \"string\" && (format === \"binary\" || format === \"base64\")\n\n return \n \n \n \n })\n }\n \n
    \n
    \n { key }\n { !required ? null :  * }\n
    \n
    \n { type }\n { format && (${format})}\n {!showCommonExtensions || !commonExt.size ? null : commonExt.entrySeq().map(([key, v]) => )}\n
    \n
    \n { prop.get(\"deprecated\") ? \"deprecated\": null }\n
    \n
    \n \n {isExecute ?
    \n {\n onChange(value, [key])\n }}\n />\n {required ? null : (\n onChangeIncludeEmpty(key, value)}\n isIncluded={included}\n isIncludedOptions={setIsIncludedOptions(key)}\n isDisabled={Array.isArray(currentValue) ? currentValue.length !== 0 : !isEmptyValue(currentValue)}\n />\n )}\n
    : null }\n
    \n
    \n }\n\n const sampleRequestBody = getDefaultRequestBodyValue(\n requestBody,\n contentType,\n activeExamplesKey,\n fn,\n )\n let language = null\n let testValueForJson = getKnownSyntaxHighlighterLanguage(sampleRequestBody)\n if (testValueForJson) {\n language = \"json\"\n }\n\n return
    \n { requestBodyDescription &&\n \n }\n {\n sampleForMediaType ? (\n \n ) : null\n }\n {\n isExecute ? (\n
    \n \n
    \n ) : (\n \n }\n includeWriteOnly={true}\n />\n )\n }\n {\n sampleForMediaType ? (\n \n ) : null\n }\n
    \n}\n\nRequestBody.propTypes = {\n userHasEditedBody: PropTypes.bool.isRequired,\n requestBody: ImPropTypes.orderedMap.isRequired,\n requestBodyValue: ImPropTypes.orderedMap.isRequired,\n requestBodyInclusionSetting: ImPropTypes.map.isRequired,\n requestBodyErrors: ImPropTypes.list.isRequired,\n getComponent: PropTypes.func.isRequired,\n getConfigs: PropTypes.func.isRequired,\n fn: PropTypes.object.isRequired,\n specSelectors: PropTypes.object.isRequired,\n contentType: PropTypes.string,\n isExecute: PropTypes.bool.isRequired,\n onChange: PropTypes.func.isRequired,\n onChangeIncludeEmpty: PropTypes.func.isRequired,\n specPath: PropTypes.array.isRequired,\n activeExamplesKey: PropTypes.string,\n updateActiveExamplesKey: PropTypes.func,\n setRetainRequestBodyValueFlag: PropTypes.func,\n oas3Actions: PropTypes.object.isRequired\n}\n\nexport default RequestBody\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\n\nexport default class ServersContainer extends React.Component {\n\n static propTypes = {\n specSelectors: PropTypes.object.isRequired,\n oas3Selectors: PropTypes.object.isRequired,\n oas3Actions: PropTypes.object.isRequired,\n getComponent: PropTypes.func.isRequired,\n }\n\n render () {\n const {specSelectors, oas3Selectors, oas3Actions, getComponent} = this.props\n\n const servers = specSelectors.servers()\n\n const Servers = getComponent(\"Servers\")\n\n return servers && servers.size ? (\n
    \n Servers\n \n
    ) : null\n }\n}","import React from \"react\"\nimport { OrderedMap } from \"immutable\"\nimport PropTypes from \"prop-types\"\nimport ImPropTypes from \"react-immutable-proptypes\"\n\nexport default class Servers extends React.Component {\n\n static propTypes = {\n servers: ImPropTypes.list.isRequired,\n currentServer: PropTypes.string.isRequired,\n setSelectedServer: PropTypes.func.isRequired,\n setServerVariableValue: PropTypes.func.isRequired,\n getServerVariable: PropTypes.func.isRequired,\n getEffectiveServerValue: PropTypes.func.isRequired\n }\n\n componentDidMount() {\n let { servers, currentServer } = this.props\n\n if(currentServer) {\n return\n }\n\n // fire 'change' event to set default 'value' of select\n this.setServer(servers.first()?.get(\"url\"))\n }\n\n UNSAFE_componentWillReceiveProps(nextProps) {\n let {\n servers,\n setServerVariableValue,\n getServerVariable\n } = nextProps\n if (this.props.currentServer !== nextProps.currentServer || this.props.servers !== nextProps.servers) {\n // Server has changed, we may need to set default values\n let currentServerDefinition = servers\n .find(v => v.get(\"url\") === nextProps.currentServer)\n let prevServerDefinition = this.props.servers\n .find(v => v.get(\"url\") === this.props.currentServer) || OrderedMap()\n \n if(!currentServerDefinition) {\n return this.setServer(servers.first().get(\"url\"))\n }\n \n let prevServerVariableDefs = prevServerDefinition.get(\"variables\") || OrderedMap()\n let prevServerVariableDefaultKey = prevServerVariableDefs.find(v => v.get(\"default\")) || OrderedMap()\n let prevServerVariableDefaultValue = prevServerVariableDefaultKey.get(\"default\")\n \n let currentServerVariableDefs = currentServerDefinition.get(\"variables\") || OrderedMap()\n let currentServerVariableDefaultKey = currentServerVariableDefs.find(v => v.get(\"default\")) || OrderedMap()\n let currentServerVariableDefaultValue = currentServerVariableDefaultKey.get(\"default\")\n \n currentServerVariableDefs.map((val, key) => {\n let currentValue = getServerVariable(nextProps.currentServer, key)\n \n // note: it is possible for both key/val to be the same across definitions,\n // but we will try to detect a change in default values between definitions\n // only set the default value if the user hasn't set one yet\n // or if the definition appears to have changed\n if (!currentValue || prevServerVariableDefaultValue !== currentServerVariableDefaultValue) {\n setServerVariableValue({\n server: nextProps.currentServer,\n key,\n val: val.get(\"default\") || \"\"\n })\n }\n })\n }\n }\n\n onServerChange =( e ) => {\n this.setServer( e.target.value )\n\n // set default variable values\n }\n\n onServerVariableValueChange = ( e ) => {\n let {\n setServerVariableValue,\n currentServer\n } = this.props\n\n let variableName = e.target.getAttribute(\"data-variable\")\n let newVariableValue = e.target.value\n\n if(typeof setServerVariableValue === \"function\") {\n setServerVariableValue({\n server: currentServer,\n key: variableName,\n val: newVariableValue\n })\n }\n }\n\n setServer = ( value ) => {\n let { setSelectedServer } = this.props\n\n setSelectedServer(value)\n }\n\n render() {\n let { servers,\n currentServer,\n getServerVariable,\n getEffectiveServerValue\n } = this.props\n\n\n let currentServerDefinition = servers.find(s => s.get(\"url\") === currentServer) || OrderedMap()\n\n let currentServerVariableDefs = currentServerDefinition.get(\"variables\") || OrderedMap()\n\n let shouldShowVariableUI = currentServerVariableDefs.size !== 0\n\n return (\n
    \n \n { shouldShowVariableUI ?\n
    \n\n
    \n Computed URL:\n \n {getEffectiveServerValue(currentServer)}\n \n
    \n

    Server variables

    \n \n \n {\n currentServerVariableDefs.entrySeq().map(([name, val]) => {\n return \n \n \n \n })\n }\n \n
    {name}\n { val.get(\"enum\") ?\n :\n \n }\n
    \n
    : null\n }\n
    \n )\n }\n}\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nexport function isOAS30(jsSpec) {\n const oasVersion = jsSpec.get(\"openapi\")\n\n return (\n typeof oasVersion === \"string\" &&\n /^3\\.0\\.([0123])(?:-rc[012])?$/.test(oasVersion)\n )\n}\n\nexport function isSwagger2(jsSpec) {\n const swaggerVersion = jsSpec.get(\"swagger\")\n\n return typeof swaggerVersion === \"string\" && swaggerVersion === \"2.0\"\n}\n\nexport function OAS3ComponentWrapFactory(Component) {\n return (Ori, system) => (props) => {\n if (typeof system.specSelectors?.isOAS3 === \"function\") {\n if (system.specSelectors.isOAS3()) {\n return \n } else {\n return \n }\n } else {\n console.warn(\"OAS3 wrapper: couldn't get spec\")\n return null\n }\n }\n}\n\nexport function OAS30ComponentWrapFactory(Component) {\n return (Ori, system) => (props) => {\n if (typeof system.specSelectors?.isOAS30 === \"function\") {\n if (system.specSelectors.isOAS30()) {\n return \n } else {\n return \n }\n } else {\n console.warn(\"OAS30 wrapper: couldn't get spec\")\n return null\n }\n }\n}\n","/**\n * @prettier\n */\nimport * as specWrapSelectors from \"./spec-extensions/wrap-selectors\"\nimport * as authWrapSelectors from \"./auth-extensions/wrap-selectors\"\nimport * as specSelectors from \"./spec-extensions/selectors\"\nimport components from \"./components\"\nimport wrapComponents from \"./wrap-components\"\nimport * as actions from \"./actions\"\nimport * as selectors from \"./selectors\"\nimport reducers from \"./reducers\"\n\nexport default function () {\n return {\n components,\n wrapComponents,\n statePlugins: {\n spec: {\n wrapSelectors: specWrapSelectors,\n selectors: specSelectors,\n },\n auth: {\n wrapSelectors: authWrapSelectors,\n },\n oas3: {\n actions,\n reducers,\n selectors,\n },\n },\n }\n}\n","import { fromJS, Map } from \"immutable\"\n\nimport {\n UPDATE_SELECTED_SERVER,\n UPDATE_REQUEST_BODY_VALUE,\n UPDATE_REQUEST_BODY_INCLUSION,\n UPDATE_ACTIVE_EXAMPLES_MEMBER,\n UPDATE_REQUEST_CONTENT_TYPE,\n UPDATE_SERVER_VARIABLE_VALUE,\n UPDATE_RESPONSE_CONTENT_TYPE,\n SET_REQUEST_BODY_VALIDATE_ERROR,\n CLEAR_REQUEST_BODY_VALIDATE_ERROR,\n CLEAR_REQUEST_BODY_VALUE, UPDATE_REQUEST_BODY_VALUE_RETAIN_FLAG,\n} from \"./actions\"\n\nexport default {\n [UPDATE_SELECTED_SERVER]: (state, { payload: { selectedServerUrl, namespace } } ) =>{\n const path = namespace ? [ namespace, \"selectedServer\"] : [ \"selectedServer\"]\n return state.setIn( path, selectedServerUrl)\n },\n [UPDATE_REQUEST_BODY_VALUE]: (state, { payload: { value, pathMethod } } ) =>{\n let [path, method] = pathMethod\n if (!Map.isMap(value)) {\n // context: application/json is always a String (instead of Map)\n return state.setIn( [ \"requestData\", path, method, \"bodyValue\" ], value)\n }\n let currentVal = state.getIn([\"requestData\", path, method, \"bodyValue\"]) || Map()\n if (!Map.isMap(currentVal)) {\n // context: user switch from application/json to application/x-www-form-urlencoded\n currentVal = Map()\n }\n let newVal\n const [...valueKeys] = value.keys()\n valueKeys.forEach((valueKey) => {\n let valueKeyVal = value.getIn([valueKey])\n if (!currentVal.has(valueKey)) {\n newVal = currentVal.setIn([valueKey, \"value\"], valueKeyVal)\n } else if (!Map.isMap(valueKeyVal)) {\n // context: user input will be received as String\n newVal = currentVal.setIn([valueKey, \"value\"], valueKeyVal)\n }\n })\n return state.setIn([\"requestData\", path, method, \"bodyValue\"], newVal)\n },\n [UPDATE_REQUEST_BODY_VALUE_RETAIN_FLAG]: (state, { payload: { value, pathMethod } } ) =>{\n let [path, method] = pathMethod\n return state.setIn([\"requestData\", path, method, \"retainBodyValue\"], value)\n },\n [UPDATE_REQUEST_BODY_INCLUSION]: (state, { payload: { value, pathMethod, name } } ) =>{\n let [path, method] = pathMethod\n return state.setIn( [ \"requestData\", path, method, \"bodyInclusion\", name ], value)\n },\n [UPDATE_ACTIVE_EXAMPLES_MEMBER]: (state, { payload: { name, pathMethod, contextType, contextName } } ) =>{\n let [path, method] = pathMethod\n return state.setIn( [ \"examples\", path, method, contextType, contextName, \"activeExample\" ], name)\n },\n [UPDATE_REQUEST_CONTENT_TYPE]: (state, { payload: { value, pathMethod } } ) =>{\n let [path, method] = pathMethod\n return state.setIn( [ \"requestData\", path, method, \"requestContentType\" ], value)\n },\n [UPDATE_RESPONSE_CONTENT_TYPE]: (state, { payload: { value, path, method } } ) =>{\n return state.setIn( [ \"requestData\", path, method, \"responseContentType\" ], value)\n },\n [UPDATE_SERVER_VARIABLE_VALUE]: (state, { payload: { server, namespace, key, val } } ) =>{\n const path = namespace ? [ namespace, \"serverVariableValues\", server, key ] : [ \"serverVariableValues\", server, key ]\n return state.setIn(path, val)\n },\n [SET_REQUEST_BODY_VALIDATE_ERROR]: (state, { payload: { path, method, validationErrors } } ) => {\n let errors = []\n errors.push(\"Required field is not provided\")\n if (validationErrors.missingBodyValue) {\n // context: is application/json or application/xml, where typeof (missing) bodyValue = String\n return state.setIn([\"requestData\", path, method, \"errors\"], fromJS(errors))\n }\n if (validationErrors.missingRequiredKeys && validationErrors.missingRequiredKeys.length > 0) {\n // context: is application/x-www-form-urlencoded, with list of missing keys\n const { missingRequiredKeys } = validationErrors\n return state.updateIn([\"requestData\", path, method, \"bodyValue\"], fromJS({}), missingKeyValues => {\n return missingRequiredKeys.reduce((bodyValue, currentMissingKey) => {\n return bodyValue.setIn([currentMissingKey, \"errors\"], fromJS(errors))\n }, missingKeyValues)\n })\n }\n console.warn(\"unexpected result: SET_REQUEST_BODY_VALIDATE_ERROR\")\n return state\n },\n [CLEAR_REQUEST_BODY_VALIDATE_ERROR]: (state, { payload: { path, method } }) => {\n const requestBodyValue = state.getIn([\"requestData\", path, method, \"bodyValue\"])\n if (!Map.isMap(requestBodyValue)) {\n return state.setIn([\"requestData\", path, method, \"errors\"], fromJS([]))\n }\n const [...valueKeys] = requestBodyValue.keys()\n if (!valueKeys) {\n return state\n }\n return state.updateIn([\"requestData\", path, method, \"bodyValue\"], fromJS({}), bodyValues => {\n return valueKeys.reduce((bodyValue, curr) => {\n return bodyValue.setIn([curr, \"errors\"], fromJS([]))\n }, bodyValues)\n })\n },\n [CLEAR_REQUEST_BODY_VALUE]: (state, { payload: { pathMethod }}) => {\n let [path, method] = pathMethod\n const requestBodyValue = state.getIn([\"requestData\", path, method, \"bodyValue\"])\n if (!requestBodyValue) {\n return state\n }\n if (!Map.isMap(requestBodyValue)) {\n return state.setIn([\"requestData\", path, method, \"bodyValue\"], \"\")\n }\n return state.setIn([\"requestData\", path, method, \"bodyValue\"], Map())\n }\n}\n","/**\n * @prettier\n */\nimport { OrderedMap, Map, List } from \"immutable\"\nimport { createSelector } from \"reselect\"\n\nimport { getDefaultRequestBodyValue } from \"./components/request-body\"\nimport { stringify } from \"../../utils\"\n\n// Helpers\n\nconst onlyOAS3 =\n (selector) =>\n (state, ...args) =>\n (system) => {\n if (system.getSystem().specSelectors.isOAS3()) {\n const selectedValue = selector(state, ...args)\n return typeof selectedValue === \"function\"\n ? selectedValue(system)\n : selectedValue\n } else {\n return null\n }\n }\n\nfunction validateRequestBodyIsRequired(selector) {\n return (...args) =>\n (system) => {\n const specJson = system.getSystem().specSelectors.specJson()\n const argsList = [...args]\n // expect argsList[0] = state\n let pathMethod = argsList[1] || []\n let isOas3RequestBodyRequired = specJson.getIn([\n \"paths\",\n ...pathMethod,\n \"requestBody\",\n \"required\",\n ])\n\n if (isOas3RequestBodyRequired) {\n return selector(...args)\n } else {\n // validation pass b/c not required\n return true\n }\n }\n}\n\nconst validateRequestBodyValueExists = (state, pathMethod) => {\n pathMethod = pathMethod || []\n let oas3RequestBodyValue = state.getIn([\n \"requestData\",\n ...pathMethod,\n \"bodyValue\",\n ])\n // context: bodyValue can be a String, or a Map\n if (!oas3RequestBodyValue) {\n return false\n }\n // validation pass if String is not empty, or if Map exists\n return true\n}\n\nexport const selectedServer = onlyOAS3((state, namespace) => {\n const path = namespace ? [namespace, \"selectedServer\"] : [\"selectedServer\"]\n return state.getIn(path) || \"\"\n})\n\nexport const requestBodyValue = onlyOAS3((state, path, method) => {\n return state.getIn([\"requestData\", path, method, \"bodyValue\"]) || null\n})\n\nexport const shouldRetainRequestBodyValue = onlyOAS3((state, path, method) => {\n return state.getIn([\"requestData\", path, method, \"retainBodyValue\"]) || false\n})\n\nexport const selectDefaultRequestBodyValue =\n (state, path, method) => (system) => {\n const { oas3Selectors, specSelectors, fn } = system.getSystem()\n\n if (specSelectors.isOAS3()) {\n const currentMediaType = oas3Selectors.requestContentType(path, method)\n if (currentMediaType) {\n return getDefaultRequestBodyValue(\n specSelectors.specResolvedSubtree([\n \"paths\",\n path,\n method,\n \"requestBody\",\n ]),\n currentMediaType,\n oas3Selectors.activeExamplesMember(\n path,\n method,\n \"requestBody\",\n \"requestBody\"\n ),\n fn\n )\n }\n }\n return null\n }\n\nexport const hasUserEditedBody = onlyOAS3((state, path, method) => (system) => {\n const { oas3Selectors, specSelectors, fn } = system\n\n let userHasEditedBody = false\n const currentMediaType = oas3Selectors.requestContentType(path, method)\n let userEditedRequestBody = oas3Selectors.requestBodyValue(path, method)\n const requestBody = specSelectors.specResolvedSubtree([\n \"paths\",\n path,\n method,\n \"requestBody\",\n ])\n\n /**\n * The only request body that can currently be edited is for Path Items that are direct values of OpenAPI.paths.\n * Path Item contained within the Callback Object or OpenAPI.webhooks (OpenAPI 3.1.0) have `Try it out`\n * disabled and thus body cannot be edited.\n */\n if (!requestBody) {\n return false\n }\n\n if (Map.isMap(userEditedRequestBody)) {\n // context is not application/json media-type\n userEditedRequestBody = stringify(\n userEditedRequestBody\n .mapEntries((kv) =>\n Map.isMap(kv[1]) ? [kv[0], kv[1].get(\"value\")] : kv\n )\n .toJS()\n )\n }\n if (List.isList(userEditedRequestBody)) {\n userEditedRequestBody = stringify(userEditedRequestBody)\n }\n\n if (currentMediaType) {\n const currentMediaTypeDefaultBodyValue = getDefaultRequestBodyValue(\n requestBody,\n currentMediaType,\n oas3Selectors.activeExamplesMember(\n path,\n method,\n \"requestBody\",\n \"requestBody\"\n ),\n fn\n )\n userHasEditedBody =\n !!userEditedRequestBody &&\n userEditedRequestBody !== currentMediaTypeDefaultBodyValue\n }\n return userHasEditedBody\n})\n\nexport const requestBodyInclusionSetting = onlyOAS3((state, path, method) => {\n return state.getIn([\"requestData\", path, method, \"bodyInclusion\"]) || Map()\n})\n\nexport const requestBodyErrors = onlyOAS3((state, path, method) => {\n return state.getIn([\"requestData\", path, method, \"errors\"]) || null\n})\n\nexport const activeExamplesMember = onlyOAS3(\n (state, path, method, type, name) => {\n return (\n state.getIn([\"examples\", path, method, type, name, \"activeExample\"]) ||\n null\n )\n }\n)\n\nexport const requestContentType = onlyOAS3((state, path, method) => {\n return (\n state.getIn([\"requestData\", path, method, \"requestContentType\"]) || null\n )\n})\n\nexport const responseContentType = onlyOAS3((state, path, method) => {\n return (\n state.getIn([\"requestData\", path, method, \"responseContentType\"]) || null\n )\n})\n\nexport const serverVariableValue = onlyOAS3((state, locationData, key) => {\n let path\n\n // locationData may take one of two forms, for backwards compatibility\n // Object: ({server, namespace?}) or String:(server)\n if (typeof locationData !== \"string\") {\n const { server, namespace } = locationData\n if (namespace) {\n path = [namespace, \"serverVariableValues\", server, key]\n } else {\n path = [\"serverVariableValues\", server, key]\n }\n } else {\n const server = locationData\n path = [\"serverVariableValues\", server, key]\n }\n\n return state.getIn(path) || null\n})\n\nexport const serverVariables = onlyOAS3((state, locationData) => {\n let path\n\n // locationData may take one of two forms, for backwards compatibility\n // Object: ({server, namespace?}) or String:(server)\n if (typeof locationData !== \"string\") {\n const { server, namespace } = locationData\n if (namespace) {\n path = [namespace, \"serverVariableValues\", server]\n } else {\n path = [\"serverVariableValues\", server]\n }\n } else {\n const server = locationData\n path = [\"serverVariableValues\", server]\n }\n\n return state.getIn(path) || OrderedMap()\n})\n\nexport const serverEffectiveValue = onlyOAS3((state, locationData) => {\n var varValues, serverValue\n\n // locationData may take one of two forms, for backwards compatibility\n // Object: ({server, namespace?}) or String:(server)\n if (typeof locationData !== \"string\") {\n const { server, namespace } = locationData\n serverValue = server\n if (namespace) {\n varValues = state.getIn([namespace, \"serverVariableValues\", serverValue])\n } else {\n varValues = state.getIn([\"serverVariableValues\", serverValue])\n }\n } else {\n serverValue = locationData\n varValues = state.getIn([\"serverVariableValues\", serverValue])\n }\n\n varValues = varValues || OrderedMap()\n let str = serverValue\n\n varValues.map((val, key) => {\n str = str.replace(new RegExp(`{${key}}`, \"g\"), val)\n })\n\n return str\n})\n\nexport const validateBeforeExecute = validateRequestBodyIsRequired(\n (state, pathMethod) => validateRequestBodyValueExists(state, pathMethod)\n)\n\nexport const validateShallowRequired = (\n state,\n {\n oas3RequiredRequestBodyContentType,\n oas3RequestContentType,\n oas3RequestBodyValue,\n }\n) => {\n let missingRequiredKeys = []\n // context: json => String; urlencoded, form-data => Map\n if (!Map.isMap(oas3RequestBodyValue)) {\n return missingRequiredKeys\n }\n let requiredKeys = []\n // Cycle through list of possible contentTypes for matching contentType and defined requiredKeys\n Object.keys(oas3RequiredRequestBodyContentType.requestContentType).forEach(\n (contentType) => {\n if (contentType === oas3RequestContentType) {\n let contentTypeVal =\n oas3RequiredRequestBodyContentType.requestContentType[contentType]\n contentTypeVal.forEach((requiredKey) => {\n if (requiredKeys.indexOf(requiredKey) < 0) {\n requiredKeys.push(requiredKey)\n }\n })\n }\n }\n )\n requiredKeys.forEach((key) => {\n let requiredKeyValue = oas3RequestBodyValue.getIn([key, \"value\"])\n if (!requiredKeyValue) {\n missingRequiredKeys.push(key)\n }\n })\n return missingRequiredKeys\n}\n\nexport const validOperationMethods = createSelector(() => [\n \"get\",\n \"put\",\n \"post\",\n \"delete\",\n \"options\",\n \"head\",\n \"patch\",\n \"trace\",\n])\n","/**\n * @prettier\n */\nimport { List, Map } from \"immutable\"\n\nimport {\n isSwagger2 as isSwagger2Helper,\n isOAS30 as isOAS30Helper,\n} from \"../helpers\"\n\n/**\n * Helpers\n */\n\nconst map = Map()\n\nexport const isSwagger2 = () => (system) => {\n const spec = system.getSystem().specSelectors.specJson()\n return isSwagger2Helper(spec)\n}\n\nexport const isOAS30 = () => (system) => {\n const spec = system.getSystem().specSelectors.specJson()\n return isOAS30Helper(spec)\n}\n\nexport const isOAS3 = () => (system) => {\n return system.getSystem().specSelectors.isOAS30()\n}\n\nfunction onlyOAS3(selector) {\n return (state, ...args) =>\n (system) => {\n if (system.specSelectors.isOAS3()) {\n const selectedValue = selector(state, ...args)\n return typeof selectedValue === \"function\"\n ? selectedValue(system)\n : selectedValue\n } else {\n return null\n }\n }\n}\n\nexport const servers = onlyOAS3(() => (system) => {\n const spec = system.specSelectors.specJson()\n return spec.get(\"servers\", map)\n})\n\nexport const callbacksOperations = onlyOAS3(\n (state, { callbacks, specPath }) =>\n (system) => {\n const validOperationMethods = system.specSelectors.validOperationMethods()\n\n if (!Map.isMap(callbacks)) return {}\n\n return callbacks\n .reduce((allOperations, callback, callbackName) => {\n if (!Map.isMap(callback)) return allOperations\n\n return callback.reduce((callbackOperations, pathItem, expression) => {\n if (!Map.isMap(pathItem)) return callbackOperations\n\n const pathItemOperations = pathItem\n .entrySeq()\n .filter(([key]) => validOperationMethods.includes(key))\n .map(([method, operation]) => ({\n operation: Map({ operation }),\n method,\n path: expression,\n callbackName,\n specPath: specPath.concat([callbackName, expression, method]),\n }))\n\n return callbackOperations.concat(pathItemOperations)\n }, List())\n }, List())\n .groupBy((operationDTO) => operationDTO.callbackName)\n .map((operations) => operations.toArray())\n .toObject()\n }\n)\n","/**\n * @prettier\n */\nimport { createSelector } from \"reselect\"\nimport { specJsonWithResolvedSubtrees } from \"../../spec/selectors\"\nimport { Map } from \"immutable\"\n\n/**\n * Helpers\n */\n\nconst map = Map()\n\nfunction onlyOAS3(selector) {\n return (ori, system) =>\n (...args) => {\n if (system.getSystem().specSelectors.isOAS3()) {\n const result = selector(...args)\n return typeof result === \"function\" ? result(system) : result\n } else {\n return ori(...args)\n }\n }\n}\n\nconst nullSelector = createSelector(() => null)\n\nconst OAS3NullSelector = onlyOAS3(nullSelector)\n\n/**\n * Wrappers\n */\n\nexport const definitions = onlyOAS3(() => (system) => {\n const spec = system.getSystem().specSelectors.specJson()\n const schemas = spec.getIn([\"components\", \"schemas\"])\n return Map.isMap(schemas) ? schemas : map\n})\n\nexport const hasHost = onlyOAS3(() => (system) => {\n const spec = system.getSystem().specSelectors.specJson()\n return spec.hasIn([\"servers\", 0])\n})\n\nexport const securityDefinitions = onlyOAS3(\n createSelector(\n specJsonWithResolvedSubtrees,\n (spec) => spec.getIn([\"components\", \"securitySchemes\"]) || null\n )\n)\n\nexport const validOperationMethods =\n (oriSelector, system) =>\n (state, ...args) => {\n if (system.specSelectors.isOAS3()) {\n return system.oas3Selectors.validOperationMethods()\n }\n\n return oriSelector(...args)\n }\n\nexport const host = OAS3NullSelector\nexport const basePath = OAS3NullSelector\nexport const consumes = OAS3NullSelector\nexport const produces = OAS3NullSelector\nexport const schemes = OAS3NullSelector\n","import React from \"react\"\nimport { OAS3ComponentWrapFactory } from \"../helpers\"\n\nexport default OAS3ComponentWrapFactory(({ Ori, ...props }) => {\n const {\n schema, getComponent, errSelectors, authorized, onAuthChange, name\n } = props\n\n const HttpAuth = getComponent(\"HttpAuth\")\n const type = schema.get(\"type\")\n\n if(type === \"http\") {\n return \n } else {\n return \n }\n})\n","import Markdown from \"./markdown\"\nimport AuthItem from \"./auth-item\"\nimport VersionStamp from \"./version-stamp\"\nimport OnlineValidatorBadge from \"./online-validator-badge\"\nimport Model from \"./model\"\nimport JsonSchema_string from \"./json-schema-string\"\n\nexport default {\n Markdown,\n AuthItem,\n JsonSchema_string,\n VersionStamp,\n model: Model,\n onlineValidatorBadge: OnlineValidatorBadge,\n}\n","import React from \"react\"\nimport { OAS3ComponentWrapFactory } from \"../helpers\"\n\nexport default OAS3ComponentWrapFactory(({ Ori, ...props }) => {\n const {\n schema,\n getComponent,\n errors,\n onChange\n } = props\n\n const format = schema && schema.get ? schema.get(\"format\") : null\n const type = schema && schema.get ? schema.get(\"type\") : null\n const Input = getComponent(\"Input\")\n\n if(type && type === \"string\" && (format && (format === \"binary\" || format === \"base64\"))) {\n return {\n onChange(e.target.files[0])\n }}\n disabled={Ori.isDisabled}/>\n } else {\n return \n }\n})\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\nimport cx from \"classnames\"\nimport { Remarkable } from \"remarkable\"\nimport { OAS3ComponentWrapFactory } from \"../helpers\"\nimport { sanitizer } from \"core/components/providers/markdown\"\n\nconst parser = new Remarkable(\"commonmark\")\nparser.block.ruler.enable([\"table\"])\nparser.set({ linkTarget: \"_blank\" })\n\nexport const Markdown = ({ source, className = \"\", getConfigs }) => {\n if(typeof source !== \"string\") {\n return null\n }\n\n if ( source ) {\n const { useUnsafeMarkdown } = getConfigs()\n const html = parser.render(source)\n const sanitized = sanitizer(html, { useUnsafeMarkdown })\n\n let trimmed\n\n if(typeof sanitized === \"string\") {\n trimmed = sanitized.trim()\n }\n\n return (\n \n )\n }\n return null\n}\nMarkdown.propTypes = {\n source: PropTypes.string,\n className: PropTypes.string,\n getConfigs: PropTypes.func,\n}\n\nMarkdown.defaultProps = {\n getConfigs: () => ({ useUnsafeMarkdown: false }),\n}\n\nexport default OAS3ComponentWrapFactory(Markdown)\n","import React, { Component } from \"react\"\nimport PropTypes from \"prop-types\"\nimport { OAS3ComponentWrapFactory } from \"../helpers\"\nimport Model from \"core/components/model\"\n\nclass ModelComponent extends Component {\n static propTypes = {\n schema: PropTypes.object.isRequired,\n name: PropTypes.string,\n getComponent: PropTypes.func.isRequired,\n getConfigs: PropTypes.func.isRequired,\n specSelectors: PropTypes.object.isRequired,\n expandDepth: PropTypes.number,\n includeReadOnly: PropTypes.bool,\n includeWriteOnly: PropTypes.bool,\n }\n\n render(){\n let { getConfigs, schema } = this.props\n let classes = [\"model-box\"]\n let isDeprecated = schema.get(\"deprecated\") === true\n let message = null\n\n if(isDeprecated) {\n classes.push(\"deprecated\")\n message = Deprecated:\n }\n\n return
    \n {message}\n \n
    \n }\n}\n\nexport default OAS3ComponentWrapFactory(ModelComponent)\n","import { OAS3ComponentWrapFactory } from \"../helpers\"\nimport OnlineValidatorBadge from \"core/components/online-validator-badge\"\n\n// OAS3 spec is now supported by the online validator.\nexport default OAS3ComponentWrapFactory(OnlineValidatorBadge)\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { OAS30ComponentWrapFactory } from \"../helpers\"\n\nexport default OAS30ComponentWrapFactory((props) => {\n const { Ori } = props\n\n return (\n \n \n \n
    OAS 3.0
    \n
    \n
    \n )\n})\n","/**\n * @prettier\n */\nimport {\n makeIsExpandable,\n getProperties,\n} from \"./json-schema-2020-12-extensions/fn\"\nimport { wrapOAS31Fn } from \"./fn\"\n\nfunction afterLoad({ fn, getSystem }) {\n // overrides for fn.jsonSchema202012\n if (fn.jsonSchema202012) {\n const isExpandable = makeIsExpandable(\n fn.jsonSchema202012.isExpandable,\n getSystem\n )\n\n Object.assign(this.fn.jsonSchema202012, { isExpandable, getProperties })\n }\n\n // wraps schema generators from samples plugin and make them specific to OpenAPI 3.1 version\n if (typeof fn.sampleFromSchema === \"function\" && fn.jsonSchema202012) {\n const wrappedFns = wrapOAS31Fn(\n {\n sampleFromSchema: fn.jsonSchema202012.sampleFromSchema,\n sampleFromSchemaGeneric: fn.jsonSchema202012.sampleFromSchemaGeneric,\n createXMLExample: fn.jsonSchema202012.createXMLExample,\n memoizedSampleFromSchema: fn.jsonSchema202012.memoizedSampleFromSchema,\n memoizedCreateXMLExample: fn.jsonSchema202012.memoizedCreateXMLExample,\n },\n getSystem()\n )\n\n Object.assign(this.fn, wrappedFns)\n }\n}\n\nexport default afterLoad\n","/**\n * @prettier\n */\nimport React from \"react\"\nimport PropTypes from \"prop-types\"\n\nimport { sanitizeUrl } from \"core/utils\"\n\nconst Contact = ({ getComponent, specSelectors }) => {\n const name = specSelectors.selectContactNameField()\n const url = specSelectors.selectContactUrl()\n const email = specSelectors.selectContactEmailField()\n\n const Link = getComponent(\"Link\")\n\n return (\n
    \n {url && (\n
    \n \n {name} - Website\n \n
    \n )}\n {email && (\n \n {url ? `Send email to ${name}` : `Contact ${name}`}\n \n )}\n
    \n )\n}\n\nContact.propTypes = {\n getComponent: PropTypes.func.isRequired,\n specSelectors: PropTypes.shape({\n selectContactNameField: PropTypes.func.isRequired,\n selectContactUrl: PropTypes.func.isRequired,\n selectContactEmailField: PropTypes.func.isRequired,\n }).isRequired,\n}\n\nexport default Contact\n","/**\n * @prettier\n */\nimport React from \"react\"\nimport PropTypes from \"prop-types\"\n\nimport { sanitizeUrl } from \"core/utils\"\n\nconst Info = ({ getComponent, specSelectors }) => {\n const version = specSelectors.version()\n const url = specSelectors.url()\n const basePath = specSelectors.basePath()\n const host = specSelectors.host()\n const summary = specSelectors.selectInfoSummaryField()\n const description = specSelectors.selectInfoDescriptionField()\n const title = specSelectors.selectInfoTitleField()\n const termsOfServiceUrl = specSelectors.selectInfoTermsOfServiceUrl()\n const externalDocsUrl = specSelectors.selectExternalDocsUrl()\n const externalDocsDesc = specSelectors.selectExternalDocsDescriptionField()\n const contact = specSelectors.contact()\n const license = specSelectors.license()\n\n const Markdown = getComponent(\"Markdown\", true)\n const Link = getComponent(\"Link\")\n const VersionStamp = getComponent(\"VersionStamp\")\n const InfoUrl = getComponent(\"InfoUrl\")\n const InfoBasePath = getComponent(\"InfoBasePath\")\n const License = getComponent(\"License\", true)\n const Contact = getComponent(\"Contact\", true)\n const JsonSchemaDialect = getComponent(\"JsonSchemaDialect\", true)\n\n return (\n
    \n
    \n

    \n {title}\n {version && }\n

    \n\n {(host || basePath) && }\n {url && }\n
    \n\n {summary &&

    {summary}

    }\n\n
    \n \n
    \n\n {termsOfServiceUrl && (\n
    \n \n Terms of service\n \n
    \n )}\n\n {contact.size > 0 && }\n\n {license.size > 0 && }\n\n {externalDocsUrl && (\n \n {externalDocsDesc || externalDocsUrl}\n \n )}\n\n \n
    \n )\n}\n\nInfo.propTypes = {\n getComponent: PropTypes.func.isRequired,\n specSelectors: PropTypes.shape({\n version: PropTypes.func.isRequired,\n url: PropTypes.func.isRequired,\n basePath: PropTypes.func.isRequired,\n host: PropTypes.func.isRequired,\n selectInfoSummaryField: PropTypes.func.isRequired,\n selectInfoDescriptionField: PropTypes.func.isRequired,\n selectInfoTitleField: PropTypes.func.isRequired,\n selectInfoTermsOfServiceUrl: PropTypes.func.isRequired,\n selectExternalDocsUrl: PropTypes.func.isRequired,\n selectExternalDocsDescriptionField: PropTypes.func.isRequired,\n contact: PropTypes.func.isRequired,\n license: PropTypes.func.isRequired,\n }).isRequired,\n}\n\nexport default Info\n","/**\n * @prettier\n */\n\nimport React from \"react\"\nimport PropTypes from \"prop-types\"\n\nimport { sanitizeUrl } from \"core/utils\"\n\nconst JsonSchemaDialect = ({ getComponent, specSelectors }) => {\n const jsonSchemaDialect = specSelectors.selectJsonSchemaDialectField()\n const jsonSchemaDialectDefault = specSelectors.selectJsonSchemaDialectDefault() // prettier-ignore\n\n const Link = getComponent(\"Link\")\n\n return (\n <>\n {jsonSchemaDialect && jsonSchemaDialect === jsonSchemaDialectDefault && (\n

    \n JSON Schema dialect:{\" \"}\n \n {jsonSchemaDialect}\n \n

    \n )}\n\n {jsonSchemaDialect && jsonSchemaDialect !== jsonSchemaDialectDefault && (\n
    \n
    \n
    \n
    \n

    Warning

    \n

    \n OpenAPI.jsonSchemaDialect field contains a\n value different from the default value of{\" \"}\n \n {jsonSchemaDialectDefault}\n \n . Values different from the default one are currently not\n supported. Please either omit the field or provide it with the\n default value.\n

    \n
    \n
    \n
    \n
    \n )}\n \n )\n}\n\nJsonSchemaDialect.propTypes = {\n getComponent: PropTypes.func.isRequired,\n specSelectors: PropTypes.shape({\n selectJsonSchemaDialectField: PropTypes.func.isRequired,\n selectJsonSchemaDialectDefault: PropTypes.func.isRequired,\n }).isRequired,\n}\n\nexport default JsonSchemaDialect\n","/**\n * @prettier\n */\nimport React from \"react\"\nimport PropTypes from \"prop-types\"\n\nimport { sanitizeUrl } from \"core/utils\"\n\nconst License = ({ getComponent, specSelectors }) => {\n const name = specSelectors.selectLicenseNameField()\n const url = specSelectors.selectLicenseUrl()\n\n const Link = getComponent(\"Link\")\n\n return (\n
    \n {url ? (\n
    \n \n {name}\n \n
    \n ) : (\n {name}\n )}\n
    \n )\n}\n\nLicense.propTypes = {\n getComponent: PropTypes.func.isRequired,\n specSelectors: PropTypes.shape({\n selectLicenseNameField: PropTypes.func.isRequired,\n selectLicenseUrl: PropTypes.func.isRequired,\n }).isRequired,\n}\n\nexport default License\n","/**\n * @prettier\n */\nimport React, { forwardRef, useCallback } from \"react\"\nimport PropTypes from \"prop-types\"\nimport ImPropTypes from \"react-immutable-proptypes\"\n\nconst decodeRefName = (uri) => {\n const unescaped = uri.replace(/~1/g, \"/\").replace(/~0/g, \"~\")\n try {\n return decodeURIComponent(unescaped)\n } catch {\n return unescaped\n }\n}\nconst getModelName = (uri) => {\n if (typeof uri === \"string\" && uri.includes(\"#/components/schemas/\")) {\n return decodeRefName(uri.replace(/^.*#\\/components\\/schemas\\//, \"\"))\n }\n return null\n}\n\nconst Model = forwardRef(({ schema, getComponent, onToggle }, ref) => {\n const JSONSchema202012 = getComponent(\"JSONSchema202012\")\n const name = getModelName(schema.get(\"$$ref\"))\n\n const handleExpand = useCallback(\n (e, expanded) => {\n onToggle(name, expanded)\n },\n [name, onToggle]\n )\n\n return (\n \n )\n})\n\nModel.propTypes = {\n schema: ImPropTypes.map.isRequired,\n getComponent: PropTypes.func.isRequired,\n getConfigs: PropTypes.func.isRequired,\n specSelectors: PropTypes.object.isRequired,\n specPath: ImPropTypes.list.isRequired,\n name: PropTypes.string,\n displayName: PropTypes.string,\n isRef: PropTypes.bool,\n required: PropTypes.bool,\n expandDepth: PropTypes.number,\n depth: PropTypes.number,\n includeReadOnly: PropTypes.bool,\n includeWriteOnly: PropTypes.bool,\n onToggle: PropTypes.func,\n}\n\nModel.defaultProps = {\n name: \"\",\n displayName: \"\",\n isRef: false,\n required: false,\n expandDepth: 0,\n depth: 1,\n includeReadOnly: false,\n includeWriteOnly: false,\n onToggle: () => {},\n}\n\nexport default Model\n","/**\n * @prettier\n */\nimport React, { useCallback, useEffect } from \"react\"\nimport PropTypes from \"prop-types\"\nimport classNames from \"classnames\"\n\nconst Models = ({\n specActions,\n specSelectors,\n layoutSelectors,\n layoutActions,\n getComponent,\n getConfigs,\n}) => {\n const schemas = specSelectors.selectSchemas()\n const hasSchemas = Object.keys(schemas).length > 0\n const schemasPath = [\"components\", \"schemas\"]\n const { docExpansion, defaultModelsExpandDepth } = getConfigs()\n const isOpenDefault = defaultModelsExpandDepth > 0 && docExpansion !== \"none\"\n const isOpen = layoutSelectors.isShown(schemasPath, isOpenDefault)\n const Collapse = getComponent(\"Collapse\")\n const JSONSchema202012 = getComponent(\"JSONSchema202012\")\n\n /**\n * Effects.\n */\n useEffect(() => {\n const isOpenAndExpanded = isOpen && defaultModelsExpandDepth > 1\n const isResolved = specSelectors.specResolvedSubtree(schemasPath) != null\n if (isOpenAndExpanded && !isResolved) {\n specActions.requestResolvedSubtree(schemasPath)\n }\n }, [isOpen, defaultModelsExpandDepth])\n\n /**\n * Event handlers.\n */\n\n const handleModelsExpand = useCallback(() => {\n layoutActions.show(schemasPath, !isOpen)\n }, [isOpen])\n const handleModelsRef = useCallback((node) => {\n if (node !== null) {\n layoutActions.readyToScroll(schemasPath, node)\n }\n }, [])\n const handleJSONSchema202012Ref = (schemaName) => (node) => {\n if (node !== null) {\n layoutActions.readyToScroll([...schemasPath, schemaName], node)\n }\n }\n const handleJSONSchema202012Expand = (schemaName) => (e, expanded) => {\n if (expanded) {\n const schemaPath = [...schemasPath, schemaName]\n const isResolved = specSelectors.specResolvedSubtree(schemaPath) != null\n if (!isResolved) {\n specActions.requestResolvedSubtree([...schemasPath, schemaName])\n }\n }\n }\n\n /**\n * Rendering.\n */\n\n if (!hasSchemas || defaultModelsExpandDepth < 0) {\n return null\n }\n\n return (\n \n

    \n \n Schemas\n \n \n \n \n

    \n \n {Object.entries(schemas).map(([schemaName, schema]) => (\n \n ))}\n \n \n )\n}\n\nModels.propTypes = {\n getComponent: PropTypes.func.isRequired,\n getConfigs: PropTypes.func.isRequired,\n specSelectors: PropTypes.shape({\n selectSchemas: PropTypes.func.isRequired,\n specResolvedSubtree: PropTypes.func.isRequired,\n }).isRequired,\n specActions: PropTypes.shape({\n requestResolvedSubtree: PropTypes.func.isRequired,\n }).isRequired,\n layoutSelectors: PropTypes.shape({\n isShown: PropTypes.func.isRequired,\n }).isRequired,\n layoutActions: PropTypes.shape({\n show: PropTypes.func.isRequired,\n readyToScroll: PropTypes.func.isRequired,\n }).isRequired,\n}\n\nexport default Models\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\n\nconst VersionPragmaFilter = ({\n bypass,\n isSwagger2,\n isOAS3,\n isOAS31,\n alsoShow,\n children,\n}) => {\n if (bypass) {\n return
    {children}
    \n }\n\n if (isSwagger2 && (isOAS3 || isOAS31)) {\n return (\n
    \n {alsoShow}\n
    \n
    \n

    Unable to render this definition

    \n

    \n swagger and openapi fields cannot be\n present in the same Swagger or OpenAPI definition. Please remove\n one of the fields.\n

    \n

    \n Supported version fields are swagger: "2.0" and\n those that match openapi: 3.x.y (for example,{\" \"}\n openapi: 3.1.0).\n

    \n
    \n
    \n
    \n )\n }\n\n if (!isSwagger2 && !isOAS3 && !isOAS31) {\n return (\n
    \n {alsoShow}\n
    \n
    \n

    Unable to render this definition

    \n

    \n The provided definition does not specify a valid version field.\n

    \n

    \n Please indicate a valid Swagger or OpenAPI version field.\n Supported version fields are swagger: "2.0" and\n those that match openapi: 3.x.y (for example,{\" \"}\n openapi: 3.1.0).\n

    \n
    \n
    \n
    \n )\n }\n\n return
    {children}
    \n}\n\nVersionPragmaFilter.propTypes = {\n isSwagger2: PropTypes.bool.isRequired,\n isOAS3: PropTypes.bool.isRequired,\n isOAS31: PropTypes.bool.isRequired,\n bypass: PropTypes.bool,\n alsoShow: PropTypes.element,\n children: PropTypes.any,\n}\n\nexport default VersionPragmaFilter\n","/**\n * @prettier\n */\nimport React from \"react\"\nimport PropTypes from \"prop-types\"\n\nconst Webhooks = ({ specSelectors, getComponent }) => {\n const operationDTOs = specSelectors.selectWebhooksOperations()\n const pathItemNames = Object.keys(operationDTOs)\n\n const OperationContainer = getComponent(\"OperationContainer\", true)\n\n if (pathItemNames.length === 0) return null\n\n return (\n
    \n

    Webhooks

    \n\n {pathItemNames.map((pathItemName) => (\n
    \n {operationDTOs[pathItemName].map((operationDTO) => (\n \n ))}\n
    \n ))}\n
    \n )\n}\n\nWebhooks.propTypes = {\n specSelectors: PropTypes.shape({\n selectWebhooksOperations: PropTypes.func.isRequired,\n }).isRequired,\n getComponent: PropTypes.func.isRequired,\n}\n\nexport default Webhooks\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nexport const isOAS31 = (jsSpec) => {\n const oasVersion = jsSpec.get(\"openapi\")\n\n return (\n typeof oasVersion === \"string\" && /^3\\.1\\.(?:[1-9]\\d*|0)$/.test(oasVersion)\n )\n}\n\n/**\n * Creates selector that returns value of the passed\n * selector when spec is OpenAPI 3.1.0., null otherwise.\n *\n * @param selector\n * @returns {function(*, ...[*]): function(*): (*|null)}\n */\nexport const createOnlyOAS31Selector =\n (selector) =>\n (state, ...args) =>\n (system) => {\n if (system.getSystem().specSelectors.isOAS31()) {\n const selectedValue = selector(state, ...args)\n return typeof selectedValue === \"function\"\n ? selectedValue(system)\n : selectedValue\n } else {\n return null\n }\n }\n\n/**\n * Creates selector wrapper that returns value of the passed\n * selector when spec is OpenAPI 3.1.0., calls original selector otherwise.\n *\n *\n * @param selector\n * @returns {function(*, *): function(*, ...[*]): (*)}\n */\nexport const createOnlyOAS31SelectorWrapper =\n (selector) =>\n (oriSelector, system) =>\n (state, ...args) => {\n if (system.getSystem().specSelectors.isOAS31()) {\n const selectedValue = selector(state, ...args)\n return typeof selectedValue === \"function\"\n ? selectedValue(oriSelector, system)\n : selectedValue\n } else {\n return oriSelector(...args)\n }\n }\n\n/**\n * Creates selector that provides system as the\n * second argument. This allows to create memoized\n * composed selectors from different plugins.\n *\n * @param selector\n * @returns {function(*, ...[*]): function(*): *}\n */\nexport const createSystemSelector =\n (selector) =>\n (state, ...args) =>\n (system) => {\n const selectedValue = selector(state, system, ...args)\n return typeof selectedValue === \"function\"\n ? selectedValue(system)\n : selectedValue\n }\n\n/* eslint-disable react/jsx-filename-extension */\n/**\n * Creates component wrapper that only wraps the component\n * when spec is OpenAPI 3.1.0. Otherwise, returns original\n * component with passed props.\n *\n * @param Component\n * @returns {function(*, *): function(*): *}\n */\nexport const createOnlyOAS31ComponentWrapper =\n (Component) => (Original, system) => (props) => {\n if (system.specSelectors.isOAS31()) {\n return (\n \n )\n }\n\n return \n }\n/* eslint-enable react/jsx-filename-extension */\n\n/**\n * Runs the fn replacement implementation when spec is OpenAPI 3.1.\n * Runs the fn original implementation otherwise.\n *\n * @param fn\n * @param system\n * @returns {{[p: string]: function(...[*]): *}}\n */\nexport const wrapOAS31Fn = (fn, system) => {\n const { fn: systemFn, specSelectors } = system\n\n return Object.fromEntries(\n Object.entries(fn).map(([name, newImpl]) => {\n const oriImpl = systemFn[name]\n const impl = (...args) =>\n specSelectors.isOAS31()\n ? newImpl(...args)\n : typeof oriImpl === \"function\"\n ? oriImpl(...args)\n : undefined\n\n return [name, impl]\n })\n )\n}\n","/**\n * @prettier\n */\nimport Webhooks from \"./components/webhooks\"\nimport License from \"./components/license\"\nimport Contact from \"./components/contact\"\nimport Info from \"./components/info\"\nimport JsonSchemaDialect from \"./components/json-schema-dialect\"\nimport VersionPragmaFilter from \"./components/version-pragma-filter\"\nimport Model from \"./components/model/model\"\nimport Models from \"./components/models/models\"\nimport LicenseWrapper from \"./wrap-components/license\"\nimport ContactWrapper from \"./wrap-components/contact\"\nimport InfoWrapper from \"./wrap-components/info\"\nimport ModelWrapper from \"./wrap-components/model\"\nimport ModelsWrapper from \"./wrap-components/models\"\nimport VersionPragmaFilterWrapper from \"./wrap-components/version-pragma-filter\"\nimport VersionStampWrapper from \"./wrap-components/version-stamp\"\nimport {\n isOAS31 as isOAS31Fn,\n createOnlyOAS31Selector as createOnlyOAS31SelectorFn,\n createSystemSelector as createSystemSelectorFn,\n} from \"./fn\"\nimport {\n license as selectLicense,\n contact as selectContact,\n webhooks as selectWebhooks,\n selectLicenseNameField,\n selectLicenseUrlField,\n selectLicenseIdentifierField,\n selectContactNameField,\n selectContactEmailField,\n selectContactUrlField,\n selectContactUrl,\n isOAS31 as selectIsOAS31,\n selectLicenseUrl,\n selectInfoTitleField,\n selectInfoSummaryField,\n selectInfoDescriptionField,\n selectInfoTermsOfServiceField,\n selectInfoTermsOfServiceUrl,\n selectExternalDocsDescriptionField,\n selectExternalDocsUrlField,\n selectExternalDocsUrl,\n selectWebhooksOperations,\n selectJsonSchemaDialectField,\n selectJsonSchemaDialectDefault,\n selectSchemas,\n} from \"./spec-extensions/selectors\"\nimport {\n isOAS3 as isOAS3SelectorWrapper,\n selectLicenseUrl as selectLicenseUrlWrapper,\n} from \"./spec-extensions/wrap-selectors\"\nimport { selectLicenseUrl as selectOAS31LicenseUrl } from \"./selectors\"\nimport JSONSchema202012KeywordExample from \"./json-schema-2020-12-extensions/components/keywords/Example\"\nimport JSONSchema202012KeywordXml from \"./json-schema-2020-12-extensions/components/keywords/Xml\"\nimport JSONSchema202012KeywordDiscriminator from \"./json-schema-2020-12-extensions/components/keywords/Discriminator/Discriminator\"\nimport JSONSchema202012KeywordExternalDocs from \"./json-schema-2020-12-extensions/components/keywords/ExternalDocs\"\nimport JSONSchema202012KeywordDescriptionWrapper from \"./json-schema-2020-12-extensions/wrap-components/keywords/Description\"\nimport JSONSchema202012KeywordDefaultWrapper from \"./json-schema-2020-12-extensions/wrap-components/keywords/Default\"\nimport JSONSchema202012KeywordPropertiesWrapper from \"./json-schema-2020-12-extensions/wrap-components/keywords/Properties\"\nimport afterLoad from \"./after-load\"\n\nconst OAS31Plugin = ({ fn }) => {\n const createSystemSelector = fn.createSystemSelector || createSystemSelectorFn\n const createOnlyOAS31Selector = fn.createOnlyOAS31Selector || createOnlyOAS31SelectorFn // prettier-ignore\n\n return {\n afterLoad,\n fn: {\n isOAS31: isOAS31Fn,\n createSystemSelector: createSystemSelectorFn,\n createOnlyOAS31Selector: createOnlyOAS31SelectorFn,\n },\n components: {\n Webhooks,\n JsonSchemaDialect,\n OAS31Info: Info,\n OAS31License: License,\n OAS31Contact: Contact,\n OAS31VersionPragmaFilter: VersionPragmaFilter,\n OAS31Model: Model,\n OAS31Models: Models,\n JSONSchema202012KeywordExample,\n JSONSchema202012KeywordXml,\n JSONSchema202012KeywordDiscriminator,\n JSONSchema202012KeywordExternalDocs,\n },\n wrapComponents: {\n InfoContainer: InfoWrapper,\n License: LicenseWrapper,\n Contact: ContactWrapper,\n VersionPragmaFilter: VersionPragmaFilterWrapper,\n VersionStamp: VersionStampWrapper,\n Model: ModelWrapper,\n Models: ModelsWrapper,\n JSONSchema202012KeywordDescription:\n JSONSchema202012KeywordDescriptionWrapper,\n JSONSchema202012KeywordDefault: JSONSchema202012KeywordDefaultWrapper,\n JSONSchema202012KeywordProperties:\n JSONSchema202012KeywordPropertiesWrapper,\n },\n statePlugins: {\n spec: {\n selectors: {\n isOAS31: createSystemSelector(selectIsOAS31),\n\n license: selectLicense,\n selectLicenseNameField,\n selectLicenseUrlField,\n selectLicenseIdentifierField: createOnlyOAS31Selector(selectLicenseIdentifierField), // prettier-ignore\n selectLicenseUrl: createSystemSelector(selectLicenseUrl),\n\n contact: selectContact,\n selectContactNameField,\n selectContactEmailField,\n selectContactUrlField,\n selectContactUrl: createSystemSelector(selectContactUrl),\n\n selectInfoTitleField,\n selectInfoSummaryField: createOnlyOAS31Selector(selectInfoSummaryField), // prettier-ignore\n selectInfoDescriptionField,\n selectInfoTermsOfServiceField,\n selectInfoTermsOfServiceUrl: createSystemSelector(selectInfoTermsOfServiceUrl), // prettier-ignore\n\n selectExternalDocsDescriptionField,\n selectExternalDocsUrlField,\n selectExternalDocsUrl: createSystemSelector(selectExternalDocsUrl),\n\n webhooks: createOnlyOAS31Selector(selectWebhooks),\n selectWebhooksOperations: createOnlyOAS31Selector(createSystemSelector(selectWebhooksOperations)), // prettier-ignore\n\n selectJsonSchemaDialectField,\n selectJsonSchemaDialectDefault,\n\n selectSchemas: createSystemSelector(selectSchemas),\n },\n wrapSelectors: {\n isOAS3: isOAS3SelectorWrapper,\n selectLicenseUrl: selectLicenseUrlWrapper,\n },\n },\n oas31: {\n selectors: {\n selectLicenseUrl: createOnlyOAS31Selector(createSystemSelector(selectOAS31LicenseUrl)), // prettier-ignore\n },\n },\n },\n }\n}\n\nexport default OAS31Plugin\n","/**\n * @prettier\n */\nimport React from \"react\"\nimport PropTypes from \"prop-types\"\n\nconst Description = ({ schema, getSystem }) => {\n if (!schema?.description) return null\n\n const { getComponent } = getSystem()\n const MarkDown = getComponent(\"Markdown\")\n\n return (\n
    \n
    \n \n
    \n
    \n )\n}\n\nDescription.propTypes = {\n schema: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]).isRequired,\n getSystem: PropTypes.func.isRequired,\n}\n\nexport default Description\n","/**\n * @prettier\n */\nimport React, { useCallback, useState } from \"react\"\nimport PropTypes from \"prop-types\"\nimport classNames from \"classnames\"\n\nimport DiscriminatorMapping from \"./DiscriminatorMapping\"\n\nconst Discriminator = ({ schema, getSystem }) => {\n const discriminator = schema?.discriminator || {}\n const { fn, getComponent } = getSystem()\n const { useIsExpandedDeeply, useComponent } = fn.jsonSchema202012\n const isExpandedDeeply = useIsExpandedDeeply()\n const isExpandable = !!discriminator.mapping\n const [expanded, setExpanded] = useState(isExpandedDeeply)\n const [expandedDeeply, setExpandedDeeply] = useState(false)\n const Accordion = useComponent(\"Accordion\")\n const ExpandDeepButton = useComponent(\"ExpandDeepButton\")\n const JSONSchemaDeepExpansionContext = getComponent(\n \"JSONSchema202012DeepExpansionContext\"\n )()\n\n /**\n * Event handlers.\n */\n const handleExpansion = useCallback(() => {\n setExpanded((prev) => !prev)\n }, [])\n const handleExpansionDeep = useCallback((e, expandedDeepNew) => {\n setExpanded(expandedDeepNew)\n setExpandedDeeply(expandedDeepNew)\n }, [])\n\n /**\n * Rendering.\n */\n if (Object.keys(discriminator).length === 0) {\n return null\n }\n\n return (\n \n
    \n {isExpandable ? (\n <>\n \n \n Discriminator\n \n \n \n \n ) : (\n \n Discriminator\n \n )}\n\n {discriminator.propertyName && (\n \n {discriminator.propertyName}\n \n )}\n \n object\n \n \n {expanded && (\n
  • \n \n
  • \n )}\n \n
    \n
    \n )\n}\n\nDiscriminator.propTypes = {\n schema: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]).isRequired,\n getSystem: PropTypes.func.isRequired,\n}\n\nexport default Discriminator\n","/**\n * @prettier\n */\nimport React from \"react\"\nimport PropTypes from \"prop-types\"\n\nconst DiscriminatorMapping = ({ discriminator }) => {\n const mapping = discriminator?.mapping || {}\n\n if (Object.keys(mapping).length === 0) {\n return null\n }\n\n return Object.entries(mapping).map(([key, value]) => (\n
    \n \n {key}\n \n \n {value}\n \n
    \n ))\n}\n\nDiscriminatorMapping.propTypes = {\n discriminator: PropTypes.shape({\n mapping: PropTypes.any,\n }),\n}\n\nDiscriminatorMapping.defaultProps = {\n mapping: undefined,\n}\n\nexport default DiscriminatorMapping\n","/**\n * @prettier\n */\nimport React from \"react\"\nimport PropTypes from \"prop-types\"\n\nconst Example = ({ schema, getSystem }) => {\n const { fn } = getSystem()\n const { hasKeyword, stringify } = fn.jsonSchema202012.useFn()\n\n if (!hasKeyword(schema, \"example\")) return null\n\n return (\n
    \n \n Example\n \n \n {stringify(schema.example)}\n \n
    \n )\n}\n\nExample.propTypes = {\n schema: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]).isRequired,\n getSystem: PropTypes.func.isRequired,\n}\n\nexport default Example\n","/**\n * @prettier\n */\nimport React, { useCallback, useState } from \"react\"\nimport PropTypes from \"prop-types\"\nimport classNames from \"classnames\"\n\nimport { sanitizeUrl } from \"core/utils\"\n\nconst ExternalDocs = ({ schema, getSystem }) => {\n const externalDocs = schema?.externalDocs || {}\n const { fn, getComponent } = getSystem()\n const { useIsExpandedDeeply, useComponent } = fn.jsonSchema202012\n const isExpandedDeeply = useIsExpandedDeeply()\n const isExpandable = !!(externalDocs.description || externalDocs.url)\n const [expanded, setExpanded] = useState(isExpandedDeeply)\n const [expandedDeeply, setExpandedDeeply] = useState(false)\n const Accordion = useComponent(\"Accordion\")\n const ExpandDeepButton = useComponent(\"ExpandDeepButton\")\n const KeywordDescription = getComponent(\"JSONSchema202012KeywordDescription\")\n const Link = getComponent(\"Link\")\n const JSONSchemaDeepExpansionContext = getComponent(\n \"JSONSchema202012DeepExpansionContext\"\n )()\n\n /**\n * Event handlers.\n */\n const handleExpansion = useCallback(() => {\n setExpanded((prev) => !prev)\n }, [])\n const handleExpansionDeep = useCallback((e, expandedDeepNew) => {\n setExpanded(expandedDeepNew)\n setExpandedDeeply(expandedDeepNew)\n }, [])\n\n /**\n * Rendering.\n */\n if (Object.keys(externalDocs).length === 0) {\n return null\n }\n\n return (\n \n
    \n {isExpandable ? (\n <>\n \n \n External documentation\n \n \n \n \n ) : (\n \n External documentation\n \n )}\n \n object\n \n \n {expanded && (\n <>\n {externalDocs.description && (\n
  • \n \n
  • \n )}\n\n {externalDocs.url && (\n
  • \n
    \n \n url\n \n \n \n {externalDocs.url}\n \n \n
    \n
  • \n )}\n \n )}\n \n
    \n
    \n )\n}\n\nExternalDocs.propTypes = {\n schema: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]).isRequired,\n getSystem: PropTypes.func.isRequired,\n}\n\nexport default ExternalDocs\n","/**\n * @prettier\n */\nimport React from \"react\"\nimport PropTypes from \"prop-types\"\nimport classNames from \"classnames\"\n\nconst Properties = ({ schema, getSystem }) => {\n const { fn } = getSystem()\n const { useComponent } = fn.jsonSchema202012\n const { getDependentRequired, getProperties } = fn.jsonSchema202012.useFn()\n const config = fn.jsonSchema202012.useConfig()\n const required = Array.isArray(schema?.required) ? schema.required : []\n const JSONSchema = useComponent(\"JSONSchema\")\n const properties = getProperties(schema, config)\n\n /**\n * Rendering.\n */\n if (Object.keys(properties).length === 0) {\n return null\n }\n\n return (\n
    \n
      \n {Object.entries(properties).map(([propertyName, propertySchema]) => {\n const isRequired = required.includes(propertyName)\n const dependentRequired = getDependentRequired(propertyName, schema)\n\n return (\n \n \n \n )\n })}\n
    \n
    \n )\n}\n\nProperties.propTypes = {\n schema: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]).isRequired,\n getSystem: PropTypes.func.isRequired,\n}\n\nexport default Properties\n","/**\n * @prettier\n */\nimport React, { useCallback, useState } from \"react\"\nimport PropTypes from \"prop-types\"\nimport classNames from \"classnames\"\n\nconst Xml = ({ schema, getSystem }) => {\n const xml = schema?.xml || {}\n const { fn, getComponent } = getSystem()\n const { useIsExpandedDeeply, useComponent } = fn.jsonSchema202012\n const isExpandedDeeply = useIsExpandedDeeply()\n const isExpandable = !!(xml.name || xml.namespace || xml.prefix)\n const [expanded, setExpanded] = useState(isExpandedDeeply)\n const [expandedDeeply, setExpandedDeeply] = useState(false)\n const Accordion = useComponent(\"Accordion\")\n const ExpandDeepButton = useComponent(\"ExpandDeepButton\")\n const JSONSchemaDeepExpansionContext = getComponent(\n \"JSONSchema202012DeepExpansionContext\"\n )()\n\n /**\n * Event handlers.\n */\n const handleExpansion = useCallback(() => {\n setExpanded((prev) => !prev)\n }, [])\n const handleExpansionDeep = useCallback((e, expandedDeepNew) => {\n setExpanded(expandedDeepNew)\n setExpandedDeeply(expandedDeepNew)\n }, [])\n\n /**\n * Rendering.\n */\n if (Object.keys(xml).length === 0) {\n return null\n }\n\n return (\n \n
    \n {isExpandable ? (\n <>\n \n \n XML\n \n \n \n \n ) : (\n \n XML\n \n )}\n {xml.attribute === true && (\n \n attribute\n \n )}\n {xml.wrapped === true && (\n \n wrapped\n \n )}\n \n object\n \n \n {expanded && (\n <>\n {xml.name && (\n
  • \n
    \n \n name\n \n \n {xml.name}\n \n
    \n
  • \n )}\n\n {xml.namespace && (\n
  • \n
    \n \n namespace\n \n \n {xml.namespace}\n \n
    \n
  • \n )}\n\n {xml.prefix && (\n
  • \n
    \n \n prefix\n \n \n {xml.prefix}\n \n
    \n
  • \n )}\n \n )}\n \n
    \n
    \n )\n}\n\nXml.propTypes = {\n schema: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]).isRequired,\n getSystem: PropTypes.func.isRequired,\n}\n\nexport default Xml\n","/**\n * @prettier\n */\nexport const makeIsExpandable = (original, getSystem) => {\n const { fn } = getSystem()\n\n if (typeof original !== \"function\") {\n return null\n }\n\n const { hasKeyword } = fn.jsonSchema202012\n\n return (schema) =>\n original(schema) ||\n hasKeyword(schema, \"example\") ||\n schema?.xml ||\n schema?.discriminator ||\n schema?.externalDocs\n}\n\nexport const getProperties = (\n schema,\n { includeReadOnly, includeWriteOnly }\n) => {\n // shortcut\n if (!schema?.properties) return {}\n\n const properties = Object.entries(schema.properties)\n const filteredProperties = properties.filter(([, value]) => {\n const isReadOnly = value?.readOnly === true\n const isWriteOnly = value?.writeOnly === true\n\n return (\n (!isReadOnly || includeReadOnly) && (!isWriteOnly || includeWriteOnly)\n )\n })\n\n return Object.fromEntries(filteredProperties)\n}\n","/**\n * @prettier\n */\nimport React from \"react\"\nimport { createOnlyOAS31ComponentWrapper } from \"../../../fn\"\n\nconst DefaultWrapper = createOnlyOAS31ComponentWrapper(\n ({ schema, getSystem, originalComponent: KeywordDefault }) => {\n const { getComponent } = getSystem()\n const KeywordDiscriminator = getComponent(\n \"JSONSchema202012KeywordDiscriminator\"\n )\n const KeywordXml = getComponent(\"JSONSchema202012KeywordXml\")\n const KeywordExample = getComponent(\"JSONSchema202012KeywordExample\")\n const KeywordExternalDocs = getComponent(\n \"JSONSchema202012KeywordExternalDocs\"\n )\n\n return (\n <>\n \n \n \n \n \n \n )\n }\n)\n\nexport default DefaultWrapper\n","/**\n * @prettier\n */\nimport DescriptionKeyword from \"../../components/keywords/Description\"\nimport { createOnlyOAS31ComponentWrapper } from \"../../../fn\"\n\nconst DescriptionWrapper = createOnlyOAS31ComponentWrapper(DescriptionKeyword)\n\nexport default DescriptionWrapper\n","/**\n * @prettier\n */\nimport PropertiesKeyword from \"../../components/keywords/Properties\"\nimport { createOnlyOAS31ComponentWrapper } from \"../../../fn\"\n\nconst PropertiesWrapper = createOnlyOAS31ComponentWrapper(PropertiesKeyword)\n\nexport default PropertiesWrapper\n","/**\n * @prettier\n */\nimport { createSelector } from \"reselect\"\n\nimport { safeBuildUrl } from \"core/utils/url\"\n\nexport const selectLicenseUrl = createSelector(\n (state, system) => system.specSelectors.url(),\n (state, system) => system.oas3Selectors.selectedServer(),\n (state, system) => system.specSelectors.selectLicenseUrlField(),\n (state, system) => system.specSelectors.selectLicenseIdentifierField(),\n (specUrl, selectedServer, url, identifier) => {\n if (url) {\n return safeBuildUrl(url, specUrl, { selectedServer })\n }\n\n if (identifier) {\n return `https://spdx.org/licenses/${identifier}.html`\n }\n\n return undefined\n }\n)\n","/**\n * @prettier\n */\nimport { List, Map } from \"immutable\"\nimport { createSelector } from \"reselect\"\n\nimport { safeBuildUrl } from \"core/utils/url\"\nimport { isOAS31 as isOAS31Fn } from \"../fn\"\n\nconst map = Map()\n\nexport const isOAS31 = createSelector(\n (state, system) => system.specSelectors.specJson(),\n isOAS31Fn\n)\n\nexport const webhooks = () => (system) => {\n return system.specSelectors.specJson().get(\"webhooks\", map)\n}\n\n/**\n * `specResolvedSubtree` selector is needed as input selector,\n * so that we regenerate the selected result whenever the lazy\n * resolution happens.\n */\nexport const selectWebhooksOperations = createSelector(\n (state, system) => system.specSelectors.webhooks(),\n (state, system) => system.specSelectors.validOperationMethods(),\n (state, system) => system.specSelectors.specResolvedSubtree([\"webhooks\"]),\n (webhooks, validOperationMethods) => {\n if (!Map.isMap(webhooks)) return {}\n\n return webhooks\n .reduce((allOperations, pathItem, pathItemName) => {\n if (!Map.isMap(pathItem)) return allOperations\n\n const pathItemOperations = pathItem\n .entrySeq()\n .filter(([key]) => validOperationMethods.includes(key))\n .map(([method, operation]) => ({\n operation: Map({ operation }),\n method,\n path: pathItemName,\n specPath: List([\"webhooks\", pathItemName, method]),\n }))\n\n return allOperations.concat(pathItemOperations)\n }, List())\n .groupBy((operationDTO) => operationDTO.path)\n .map((operations) => operations.toArray())\n .toObject()\n }\n)\n\nexport const license = () => (system) => {\n return system.specSelectors.info().get(\"license\", map)\n}\n\nexport const selectLicenseNameField = () => (system) => {\n return system.specSelectors.license().get(\"name\", \"License\")\n}\n\nexport const selectLicenseUrlField = () => (system) => {\n return system.specSelectors.license().get(\"url\")\n}\n\nexport const selectLicenseUrl = createSelector(\n (state, system) => system.specSelectors.url(),\n (state, system) => system.oas3Selectors.selectedServer(),\n (state, system) => system.specSelectors.selectLicenseUrlField(),\n (specUrl, selectedServer, url) => {\n if (url) {\n return safeBuildUrl(url, specUrl, { selectedServer })\n }\n\n return undefined\n }\n)\n\nexport const selectLicenseIdentifierField = () => (system) => {\n return system.specSelectors.license().get(\"identifier\")\n}\n\nexport const contact = () => (system) => {\n return system.specSelectors.info().get(\"contact\", map)\n}\n\nexport const selectContactNameField = () => (system) => {\n return system.specSelectors.contact().get(\"name\", \"the developer\")\n}\n\nexport const selectContactEmailField = () => (system) => {\n return system.specSelectors.contact().get(\"email\")\n}\n\nexport const selectContactUrlField = () => (system) => {\n return system.specSelectors.contact().get(\"url\")\n}\n\nexport const selectContactUrl = createSelector(\n (state, system) => system.specSelectors.url(),\n (state, system) => system.oas3Selectors.selectedServer(),\n (state, system) => system.specSelectors.selectContactUrlField(),\n (specUrl, selectedServer, url) => {\n if (url) {\n return safeBuildUrl(url, specUrl, { selectedServer })\n }\n\n return undefined\n }\n)\n\nexport const selectInfoTitleField = () => (system) => {\n return system.specSelectors.info().get(\"title\")\n}\n\nexport const selectInfoSummaryField = () => (system) => {\n return system.specSelectors.info().get(\"summary\")\n}\n\nexport const selectInfoDescriptionField = () => (system) => {\n return system.specSelectors.info().get(\"description\")\n}\n\nexport const selectInfoTermsOfServiceField = () => (system) => {\n return system.specSelectors.info().get(\"termsOfService\")\n}\n\nexport const selectInfoTermsOfServiceUrl = createSelector(\n (state, system) => system.specSelectors.url(),\n (state, system) => system.oas3Selectors.selectedServer(),\n (state, system) => system.specSelectors.selectInfoTermsOfServiceField(),\n (specUrl, selectedServer, termsOfService) => {\n if (termsOfService) {\n return safeBuildUrl(termsOfService, specUrl, { selectedServer })\n }\n\n return undefined\n }\n)\n\nexport const selectExternalDocsDescriptionField = () => (system) => {\n return system.specSelectors.externalDocs().get(\"description\")\n}\n\nexport const selectExternalDocsUrlField = () => (system) => {\n return system.specSelectors.externalDocs().get(\"url\")\n}\n\nexport const selectExternalDocsUrl = createSelector(\n (state, system) => system.specSelectors.url(),\n (state, system) => system.oas3Selectors.selectedServer(),\n (state, system) => system.specSelectors.selectExternalDocsUrlField(),\n (specUrl, selectedServer, url) => {\n if (url) {\n return safeBuildUrl(url, specUrl, { selectedServer })\n }\n\n return undefined\n }\n)\n\nexport const selectJsonSchemaDialectField = () => (system) => {\n return system.specSelectors.specJson().get(\"jsonSchemaDialect\")\n}\n\nexport const selectJsonSchemaDialectDefault = () =>\n \"https://spec.openapis.org/oas/3.1/dialect/base\"\n\nexport const selectSchemas = createSelector(\n (state, system) => system.specSelectors.definitions(),\n (state, system) =>\n system.specSelectors.specResolvedSubtree([\"components\", \"schemas\"]),\n\n (rawSchemas, resolvedSchemas) => {\n if (!Map.isMap(rawSchemas)) return {}\n if (!Map.isMap(resolvedSchemas)) return rawSchemas.toJS()\n\n return Object.entries(rawSchemas.toJS()).reduce(\n (acc, [schemaName, rawSchema]) => {\n const resolvedSchema = resolvedSchemas.get(schemaName)\n acc[schemaName] = resolvedSchema?.toJS() || rawSchema\n return acc\n },\n {}\n )\n }\n)\n","/**\n * @prettier\n */\n\nimport { createOnlyOAS31SelectorWrapper } from \"../fn\"\n\nexport const isOAS3 =\n (oriSelector, system) =>\n (state, ...args) => {\n const isOAS31 = system.specSelectors.isOAS31()\n return isOAS31 || oriSelector(...args)\n }\n\nexport const selectLicenseUrl = createOnlyOAS31SelectorWrapper(\n () => (oriSelector, system) => {\n return system.oas31Selectors.selectLicenseUrl()\n }\n)\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { createOnlyOAS31ComponentWrapper } from \"../fn\"\n\nconst ContactWrapper = createOnlyOAS31ComponentWrapper(({ getSystem }) => {\n const system = getSystem()\n const OAS31Contact = system.getComponent(\"OAS31Contact\", true)\n\n return \n})\n\nexport default ContactWrapper\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { createOnlyOAS31ComponentWrapper } from \"../fn\"\n\nconst InfoWrapper = createOnlyOAS31ComponentWrapper(({ getSystem }) => {\n const system = getSystem()\n const OAS31Info = system.getComponent(\"OAS31Info\", true)\n\n return \n})\n\nexport default InfoWrapper\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { createOnlyOAS31ComponentWrapper } from \"../fn\"\n\nconst LicenseWrapper = createOnlyOAS31ComponentWrapper(({ getSystem }) => {\n const system = getSystem()\n const OAS31License = system.getComponent(\"OAS31License\", true)\n\n return \n})\n\nexport default LicenseWrapper\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { createOnlyOAS31ComponentWrapper } from \"../fn\"\nimport {\n makeIsExpandable,\n getProperties,\n} from \"../json-schema-2020-12-extensions/fn\"\n\nconst ModelWrapper = createOnlyOAS31ComponentWrapper(\n ({ getSystem, ...props }) => {\n const system = getSystem()\n const { getComponent, fn, getConfigs } = system\n const configs = getConfigs()\n\n const Model = getComponent(\"OAS31Model\")\n const JSONSchema = getComponent(\"JSONSchema202012\")\n const Keyword$schema = getComponent(\"JSONSchema202012Keyword$schema\")\n const Keyword$vocabulary = getComponent(\n \"JSONSchema202012Keyword$vocabulary\"\n )\n const Keyword$id = getComponent(\"JSONSchema202012Keyword$id\")\n const Keyword$anchor = getComponent(\"JSONSchema202012Keyword$anchor\")\n const Keyword$dynamicAnchor = getComponent(\n \"JSONSchema202012Keyword$dynamicAnchor\"\n )\n const Keyword$ref = getComponent(\"JSONSchema202012Keyword$ref\")\n const Keyword$dynamicRef = getComponent(\n \"JSONSchema202012Keyword$dynamicRef\"\n )\n const Keyword$defs = getComponent(\"JSONSchema202012Keyword$defs\")\n const Keyword$comment = getComponent(\"JSONSchema202012Keyword$comment\")\n const KeywordAllOf = getComponent(\"JSONSchema202012KeywordAllOf\")\n const KeywordAnyOf = getComponent(\"JSONSchema202012KeywordAnyOf\")\n const KeywordOneOf = getComponent(\"JSONSchema202012KeywordOneOf\")\n const KeywordNot = getComponent(\"JSONSchema202012KeywordNot\")\n const KeywordIf = getComponent(\"JSONSchema202012KeywordIf\")\n const KeywordThen = getComponent(\"JSONSchema202012KeywordThen\")\n const KeywordElse = getComponent(\"JSONSchema202012KeywordElse\")\n const KeywordDependentSchemas = getComponent(\n \"JSONSchema202012KeywordDependentSchemas\"\n )\n const KeywordPrefixItems = getComponent(\n \"JSONSchema202012KeywordPrefixItems\"\n )\n const KeywordItems = getComponent(\"JSONSchema202012KeywordItems\")\n const KeywordContains = getComponent(\"JSONSchema202012KeywordContains\")\n const KeywordProperties = getComponent(\"JSONSchema202012KeywordProperties\")\n const KeywordPatternProperties = getComponent(\n \"JSONSchema202012KeywordPatternProperties\"\n )\n const KeywordAdditionalProperties = getComponent(\n \"JSONSchema202012KeywordAdditionalProperties\"\n )\n const KeywordPropertyNames = getComponent(\n \"JSONSchema202012KeywordPropertyNames\"\n )\n const KeywordUnevaluatedItems = getComponent(\n \"JSONSchema202012KeywordUnevaluatedItems\"\n )\n const KeywordUnevaluatedProperties = getComponent(\n \"JSONSchema202012KeywordUnevaluatedProperties\"\n )\n const KeywordType = getComponent(\"JSONSchema202012KeywordType\")\n const KeywordEnum = getComponent(\"JSONSchema202012KeywordEnum\")\n const KeywordConst = getComponent(\"JSONSchema202012KeywordConst\")\n const KeywordConstraint = getComponent(\"JSONSchema202012KeywordConstraint\")\n const KeywordDependentRequired = getComponent(\n \"JSONSchema202012KeywordDependentRequired\"\n )\n const KeywordContentSchema = getComponent(\n \"JSONSchema202012KeywordContentSchema\"\n )\n const KeywordTitle = getComponent(\"JSONSchema202012KeywordTitle\")\n const KeywordDescription = getComponent(\n \"JSONSchema202012KeywordDescription\"\n )\n const KeywordDefault = getComponent(\"JSONSchema202012KeywordDefault\")\n const KeywordDeprecated = getComponent(\"JSONSchema202012KeywordDeprecated\")\n const KeywordReadOnly = getComponent(\"JSONSchema202012KeywordReadOnly\")\n const KeywordWriteOnly = getComponent(\"JSONSchema202012KeywordWriteOnly\")\n const Accordion = getComponent(\"JSONSchema202012Accordion\")\n const ExpandDeepButton = getComponent(\"JSONSchema202012ExpandDeepButton\")\n const ChevronRightIcon = getComponent(\"JSONSchema202012ChevronRightIcon\")\n const withSchemaContext = getComponent(\"withJSONSchema202012Context\")\n\n const ModelWithJSONSchemaContext = withSchemaContext(Model, {\n config: {\n default$schema: \"https://spec.openapis.org/oas/3.1/dialect/base\",\n defaultExpandedLevels: configs.defaultModelExpandDepth,\n includeReadOnly: Boolean(props.includeReadOnly),\n includeWriteOnly: Boolean(props.includeWriteOnly),\n },\n components: {\n JSONSchema,\n Keyword$schema,\n Keyword$vocabulary,\n Keyword$id,\n Keyword$anchor,\n Keyword$dynamicAnchor,\n Keyword$ref,\n Keyword$dynamicRef,\n Keyword$defs,\n Keyword$comment,\n KeywordAllOf,\n KeywordAnyOf,\n KeywordOneOf,\n KeywordNot,\n KeywordIf,\n KeywordThen,\n KeywordElse,\n KeywordDependentSchemas,\n KeywordPrefixItems,\n KeywordItems,\n KeywordContains,\n KeywordProperties,\n KeywordPatternProperties,\n KeywordAdditionalProperties,\n KeywordPropertyNames,\n KeywordUnevaluatedItems,\n KeywordUnevaluatedProperties,\n KeywordType,\n KeywordEnum,\n KeywordConst,\n KeywordConstraint,\n KeywordDependentRequired,\n KeywordContentSchema,\n KeywordTitle,\n KeywordDescription,\n KeywordDefault,\n KeywordDeprecated,\n KeywordReadOnly,\n KeywordWriteOnly,\n Accordion,\n ExpandDeepButton,\n ChevronRightIcon,\n },\n fn: {\n upperFirst: fn.upperFirst,\n isExpandable: makeIsExpandable(\n fn.jsonSchema202012.isExpandable,\n getSystem\n ),\n getProperties,\n },\n })\n\n return \n }\n)\n\nexport default ModelWrapper\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { createOnlyOAS31ComponentWrapper } from \"../fn\"\n\nconst ModelsWrapper = createOnlyOAS31ComponentWrapper(({ getSystem }) => {\n const { getComponent, fn, getConfigs } = getSystem()\n const configs = getConfigs()\n\n if (ModelsWrapper.ModelsWithJSONSchemaContext) {\n return \n }\n\n const Models = getComponent(\"OAS31Models\", true)\n const JSONSchema = getComponent(\"JSONSchema202012\")\n const Keyword$schema = getComponent(\"JSONSchema202012Keyword$schema\")\n const Keyword$vocabulary = getComponent(\"JSONSchema202012Keyword$vocabulary\")\n const Keyword$id = getComponent(\"JSONSchema202012Keyword$id\")\n const Keyword$anchor = getComponent(\"JSONSchema202012Keyword$anchor\")\n const Keyword$dynamicAnchor = getComponent(\n \"JSONSchema202012Keyword$dynamicAnchor\"\n )\n const Keyword$ref = getComponent(\"JSONSchema202012Keyword$ref\")\n const Keyword$dynamicRef = getComponent(\"JSONSchema202012Keyword$dynamicRef\")\n const Keyword$defs = getComponent(\"JSONSchema202012Keyword$defs\")\n const Keyword$comment = getComponent(\"JSONSchema202012Keyword$comment\")\n const KeywordAllOf = getComponent(\"JSONSchema202012KeywordAllOf\")\n const KeywordAnyOf = getComponent(\"JSONSchema202012KeywordAnyOf\")\n const KeywordOneOf = getComponent(\"JSONSchema202012KeywordOneOf\")\n const KeywordNot = getComponent(\"JSONSchema202012KeywordNot\")\n const KeywordIf = getComponent(\"JSONSchema202012KeywordIf\")\n const KeywordThen = getComponent(\"JSONSchema202012KeywordThen\")\n const KeywordElse = getComponent(\"JSONSchema202012KeywordElse\")\n const KeywordDependentSchemas = getComponent(\n \"JSONSchema202012KeywordDependentSchemas\"\n )\n const KeywordPrefixItems = getComponent(\"JSONSchema202012KeywordPrefixItems\")\n const KeywordItems = getComponent(\"JSONSchema202012KeywordItems\")\n const KeywordContains = getComponent(\"JSONSchema202012KeywordContains\")\n const KeywordProperties = getComponent(\"JSONSchema202012KeywordProperties\")\n const KeywordPatternProperties = getComponent(\n \"JSONSchema202012KeywordPatternProperties\"\n )\n const KeywordAdditionalProperties = getComponent(\n \"JSONSchema202012KeywordAdditionalProperties\"\n )\n const KeywordPropertyNames = getComponent(\n \"JSONSchema202012KeywordPropertyNames\"\n )\n const KeywordUnevaluatedItems = getComponent(\n \"JSONSchema202012KeywordUnevaluatedItems\"\n )\n const KeywordUnevaluatedProperties = getComponent(\n \"JSONSchema202012KeywordUnevaluatedProperties\"\n )\n const KeywordType = getComponent(\"JSONSchema202012KeywordType\")\n const KeywordEnum = getComponent(\"JSONSchema202012KeywordEnum\")\n const KeywordConst = getComponent(\"JSONSchema202012KeywordConst\")\n const KeywordConstraint = getComponent(\"JSONSchema202012KeywordConstraint\")\n const KeywordDependentRequired = getComponent(\n \"JSONSchema202012KeywordDependentRequired\"\n )\n const KeywordContentSchema = getComponent(\n \"JSONSchema202012KeywordContentSchema\"\n )\n const KeywordTitle = getComponent(\"JSONSchema202012KeywordTitle\")\n const KeywordDescription = getComponent(\"JSONSchema202012KeywordDescription\")\n const KeywordDefault = getComponent(\"JSONSchema202012KeywordDefault\")\n const KeywordDeprecated = getComponent(\"JSONSchema202012KeywordDeprecated\")\n const KeywordReadOnly = getComponent(\"JSONSchema202012KeywordReadOnly\")\n const KeywordWriteOnly = getComponent(\"JSONSchema202012KeywordWriteOnly\")\n const Accordion = getComponent(\"JSONSchema202012Accordion\")\n const ExpandDeepButton = getComponent(\"JSONSchema202012ExpandDeepButton\")\n const ChevronRightIcon = getComponent(\"JSONSchema202012ChevronRightIcon\")\n const withSchemaContext = getComponent(\"withJSONSchema202012Context\")\n\n // we cache the HOC as recreating it with every re-render is quite expensive\n ModelsWrapper.ModelsWithJSONSchemaContext = withSchemaContext(Models, {\n config: {\n default$schema: \"https://spec.openapis.org/oas/3.1/dialect/base\",\n defaultExpandedLevels: configs.defaultModelsExpandDepth - 1,\n includeReadOnly: true,\n includeWriteOnly: true,\n },\n components: {\n JSONSchema,\n Keyword$schema,\n Keyword$vocabulary,\n Keyword$id,\n Keyword$anchor,\n Keyword$dynamicAnchor,\n Keyword$ref,\n Keyword$dynamicRef,\n Keyword$defs,\n Keyword$comment,\n KeywordAllOf,\n KeywordAnyOf,\n KeywordOneOf,\n KeywordNot,\n KeywordIf,\n KeywordThen,\n KeywordElse,\n KeywordDependentSchemas,\n KeywordPrefixItems,\n KeywordItems,\n KeywordContains,\n KeywordProperties,\n KeywordPatternProperties,\n KeywordAdditionalProperties,\n KeywordPropertyNames,\n KeywordUnevaluatedItems,\n KeywordUnevaluatedProperties,\n KeywordType,\n KeywordEnum,\n KeywordConst,\n KeywordConstraint,\n KeywordDependentRequired,\n KeywordContentSchema,\n KeywordTitle,\n KeywordDescription,\n KeywordDefault,\n KeywordDeprecated,\n KeywordReadOnly,\n KeywordWriteOnly,\n Accordion,\n ExpandDeepButton,\n ChevronRightIcon,\n },\n fn: {\n upperFirst: fn.upperFirst,\n isExpandable: fn.jsonSchema202012.isExpandable,\n getProperties: fn.jsonSchema202012.getProperties,\n },\n })\n\n return \n})\n\nModelsWrapper.ModelsWithJSONSchemaContext = null\n\nexport default ModelsWrapper\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nconst VersionPragmaFilterWrapper = (Original, system) => (props) => {\n const isOAS31 = system.specSelectors.isOAS31()\n\n const OAS31VersionPragmaFilter = system.getComponent(\n \"OAS31VersionPragmaFilter\"\n )\n\n return \n}\n\nexport default VersionPragmaFilterWrapper\n","/**\n * @prettier\n */\nimport React from \"react\"\n\nimport { createOnlyOAS31ComponentWrapper } from \"../fn\"\n\nconst VersionStampWrapper = createOnlyOAS31ComponentWrapper(\n ({ originalComponent: Original, ...restProps }) => (\n \n \n \n
    OAS 3.1
    \n
    \n
    \n )\n)\n\nexport default VersionStampWrapper\n","let engaged = false\n\nexport default function() {\n\n return {\n statePlugins: {\n spec: {\n wrapActions: {\n updateSpec: (ori) => (...args) => {\n engaged = true\n return ori(...args)\n },\n updateJsonSpec: (ori, system) => (...args) => {\n const cb = system.getConfigs().onComplete\n if(engaged && typeof cb === \"function\") {\n // call `onComplete` on next tick, which allows React to\n // reconcile the DOM before we notify the user\n setTimeout(cb, 0)\n engaged = false\n }\n\n return ori(...args)\n }\n }\n }\n }\n }\n}\n","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_instance_repeat_18ab8b74__[\"default\"] });","import win from \"../../window\"\nimport { Map } from \"immutable\"\n\n/**\n * if duplicate key name existed from FormData entries,\n * we mutated the key name by appending a hashIdx\n * @param {String} k - possibly mutated key name\n * @return {String} - src key name\n */\nconst extractKey = (k) => {\n const hashIdx = \"_**[]\"\n if (k.indexOf(hashIdx) < 0) {\n return k\n }\n return k.split(hashIdx)[0].trim()\n}\n\nconst escapeShell = (str) => {\n if (str === \"-d \") {\n return str\n }\n // eslint-disable-next-line no-useless-escape\n if (!/^[_\\/-]/g.test(str))\n return (\"'\" + str\n .replace(/'/g, \"'\\\\''\") + \"'\")\n else\n return str\n}\n\nconst escapeCMD = (str) => {\n str = str\n .replace(/\\^/g, \"^^\")\n .replace(/\\\\\"/g, \"\\\\\\\\\\\"\")\n .replace(/\"/g, \"\\\"\\\"\")\n .replace(/\\n/g, \"^\\n\")\n if (str === \"-d \") {\n return str\n .replace(/-d /g, \"-d ^\\n\")\n }\n // eslint-disable-next-line no-useless-escape\n if (!/^[_\\/-]/g.test(str))\n return \"\\\"\" + str + \"\\\"\"\n else\n return str\n}\n\nconst escapePowershell = (str) => {\n if (str === \"-d \") {\n return str\n }\n if (/\\n/.test(str)) {\n return \"@\\\"\\n\" + str.replace(/\"/g, \"\\\\\\\"\").replace(/`/g, \"``\").replace(/\\$/, \"`$\") + \"\\n\\\"@\"\n }\n // eslint-disable-next-line no-useless-escape\n if (!/^[_\\/-]/g.test(str))\n return \"'\" + str\n .replace(/\"/g, \"\\\"\\\"\")\n .replace(/'/g, \"''\") + \"'\"\n else\n return str\n}\n\nfunction getStringBodyOfMap(request) {\n let curlifyToJoin = []\n for (let [k, v] of request.get(\"body\").entrySeq()) {\n let extractedKey = extractKey(k)\n if (v instanceof win.File) {\n curlifyToJoin.push(` \"${extractedKey}\": {\\n \"name\": \"${v.name}\"${v.type ? `,\\n \"type\": \"${v.type}\"` : \"\"}\\n }`)\n } else {\n curlifyToJoin.push(` \"${extractedKey}\": ${JSON.stringify(v, null, 2).replace(/(\\r\\n|\\r|\\n)/g, \"\\n \")}`)\n }\n }\n return `{\\n${curlifyToJoin.join(\",\\n\")}\\n}`\n}\n\nconst curlify = (request, escape, newLine, ext = \"\") => {\n let isMultipartFormDataRequest = false\n let curlified = \"\"\n const addWords = (...args) => curlified += \" \" + args.map(escape).join(\" \")\n const addWordsWithoutLeadingSpace = (...args) => curlified += args.map(escape).join(\" \")\n const addNewLine = () => curlified += ` ${newLine}`\n const addIndent = (level = 1) => curlified += \" \".repeat(level)\n let headers = request.get(\"headers\")\n curlified += \"curl\" + ext\n\n if (request.has(\"curlOptions\")) {\n addWords(...request.get(\"curlOptions\"))\n }\n\n addWords(\"-X\", request.get(\"method\"))\n\n addNewLine()\n addIndent()\n addWordsWithoutLeadingSpace(`${request.get(\"url\")}`)\n\n if (headers && headers.size) {\n for (let p of request.get(\"headers\").entries()) {\n addNewLine()\n addIndent()\n let [h, v] = p\n addWordsWithoutLeadingSpace(\"-H\", `${h}: ${v}`)\n isMultipartFormDataRequest = isMultipartFormDataRequest || /^content-type$/i.test(h) && /^multipart\\/form-data$/i.test(v)\n }\n }\n\n const body = request.get(\"body\")\n if (body) {\n if (isMultipartFormDataRequest && [\"POST\", \"PUT\", \"PATCH\"].includes(request.get(\"method\"))) {\n for (let [k, v] of body.entrySeq()) {\n let extractedKey = extractKey(k)\n addNewLine()\n addIndent()\n addWordsWithoutLeadingSpace(\"-F\")\n if (v instanceof win.File) {\n addWords(`${extractedKey}=@${v.name}${v.type ? `;type=${v.type}` : \"\"}`)\n } else {\n addWords(`${extractedKey}=${v}`)\n }\n }\n } else if(body instanceof win.File) {\n addNewLine()\n addIndent()\n addWordsWithoutLeadingSpace(`--data-binary '@${body.name}'`)\n } else {\n addNewLine()\n addIndent()\n addWordsWithoutLeadingSpace(\"-d \")\n let reqBody = body\n if (!Map.isMap(reqBody)) {\n if (typeof reqBody !== \"string\") {\n reqBody = JSON.stringify(reqBody)\n }\n addWordsWithoutLeadingSpace(reqBody)\n } else {\n addWordsWithoutLeadingSpace(getStringBodyOfMap(request))\n }\n }\n } else if (!body && request.get(\"method\") === \"POST\") {\n addNewLine()\n addIndent()\n addWordsWithoutLeadingSpace(\"-d ''\")\n }\n\n return curlified\n}\n\n// eslint-disable-next-line camelcase\nexport const requestSnippetGenerator_curl_powershell = (request) => {\n return curlify(request, escapePowershell, \"`\\n\", \".exe\")\n}\n\n// eslint-disable-next-line camelcase\nexport const requestSnippetGenerator_curl_bash = (request) => {\n return curlify(request, escapeShell, \"\\\\\\n\")\n}\n\n// eslint-disable-next-line camelcase\nexport const requestSnippetGenerator_curl_cmd = (request) => {\n return curlify(request, escapeCMD, \"^\\n\")\n}\n","import * as fn from \"./fn\"\nimport * as selectors from \"./selectors\"\nimport RequestSnippets from \"./request-snippets\"\nexport default () => {\n return {\n components: {\n RequestSnippets\n },\n fn,\n statePlugins: {\n requestSnippets: {\n selectors\n }\n }\n }\n}\n","import React, { useRef, useEffect, useState } from \"react\"\nimport PropTypes from \"prop-types\"\nimport get from \"lodash/get\"\nimport isFunction from \"lodash/isFunction\"\nimport { CopyToClipboard } from \"react-copy-to-clipboard\"\nimport { SyntaxHighlighter, getStyle } from \"core/syntax-highlighting\"\n\nconst style = {\n cursor: \"pointer\",\n lineHeight: 1,\n display: \"inline-flex\",\n backgroundColor: \"rgb(250, 250, 250)\",\n paddingBottom: \"0\",\n paddingTop: \"0\",\n border: \"1px solid rgb(51, 51, 51)\",\n borderRadius: \"4px 4px 0 0\",\n boxShadow: \"none\",\n borderBottom: \"none\"\n}\n\nconst activeStyle = {\n cursor: \"pointer\",\n lineHeight: 1,\n display: \"inline-flex\",\n backgroundColor: \"rgb(51, 51, 51)\",\n boxShadow: \"none\",\n border: \"1px solid rgb(51, 51, 51)\",\n paddingBottom: \"0\",\n paddingTop: \"0\",\n borderRadius: \"4px 4px 0 0\",\n marginTop: \"-5px\",\n marginRight: \"-5px\",\n marginLeft: \"-5px\",\n zIndex: \"9999\",\n borderBottom: \"none\"\n}\n\nconst RequestSnippets = ({ request, requestSnippetsSelectors, getConfigs }) => {\n const config = isFunction(getConfigs) ? getConfigs() : null\n const canSyntaxHighlight = get(config, \"syntaxHighlight\") !== false && get(config, \"syntaxHighlight.activated\", true)\n const rootRef = useRef(null)\n\n const [activeLanguage, setActiveLanguage] = useState(requestSnippetsSelectors.getSnippetGenerators()?.keySeq().first())\n const [isExpanded, setIsExpanded] = useState(requestSnippetsSelectors?.getDefaultExpanded())\n useEffect(() => {\n const doIt = () => {\n\n }\n doIt()\n }, [])\n useEffect(() => {\n const childNodes = Array\n .from(rootRef.current.childNodes)\n .filter(node => !!node.nodeType && node.classList?.contains(\"curl-command\"))\n // eslint-disable-next-line no-use-before-define\n childNodes.forEach(node => node.addEventListener(\"mousewheel\", handlePreventYScrollingBeyondElement, { passive: false }))\n\n return () => {\n // eslint-disable-next-line no-use-before-define\n childNodes.forEach(node => node.removeEventListener(\"mousewheel\", handlePreventYScrollingBeyondElement))\n }\n }, [request])\n\n const snippetGenerators = requestSnippetsSelectors.getSnippetGenerators()\n const activeGenerator = snippetGenerators.get(activeLanguage)\n const snippet = activeGenerator.get(\"fn\")(request)\n\n const handleGenChange = (key) => {\n const needsChange = activeLanguage !== key\n if (needsChange) {\n setActiveLanguage(key)\n }\n }\n\n const handleSetIsExpanded = () => {\n setIsExpanded(!isExpanded)\n }\n\n const handleGetBtnStyle = (key) => {\n if (key === activeLanguage) {\n return activeStyle\n }\n return style\n }\n\n const handlePreventYScrollingBeyondElement = (e) => {\n const { target, deltaY } = e\n const { scrollHeight: contentHeight, offsetHeight: visibleHeight, scrollTop } = target\n const scrollOffset = visibleHeight + scrollTop\n const isElementScrollable = contentHeight > visibleHeight\n const isScrollingPastTop = scrollTop === 0 && deltaY < 0\n const isScrollingPastBottom = scrollOffset >= contentHeight && deltaY > 0\n\n if (isElementScrollable && (isScrollingPastTop || isScrollingPastBottom)) {\n e.preventDefault()\n }\n }\n\n const SnippetComponent = canSyntaxHighlight\n ? \n {snippet}\n \n :\n \n\n return (\n
    \n
    \n handleSetIsExpanded()}\n style={{ cursor: \"pointer\" }}\n >Snippets\n handleSetIsExpanded()}\n style={{ border: \"none\", background: \"none\" }}\n title={isExpanded ? \"Collapse operation\" : \"Expand operation\"}\n >\n \n \n \n \n
    \n {\n isExpanded &&
    \n
    \n {\n snippetGenerators.entrySeq().map(([key, gen]) => {\n return (
    handleGenChange(key)}>\n

    {gen.get(\"title\")}

    \n
    )\n })\n }\n
    \n
    \n \n
    \n
    \n {SnippetComponent}\n
    \n
    \n }\n
    \n ) \n}\n\nRequestSnippets.propTypes = {\n request: PropTypes.object.isRequired,\n requestSnippetsSelectors: PropTypes.object.isRequired,\n getConfigs: PropTypes.object.isRequired,\n requestSnippetsActions: PropTypes.object,\n}\n\nexport default RequestSnippets\n","import { createSelector } from \"reselect\"\nimport { Map } from \"immutable\"\n\nconst state = state => state || Map()\n\nexport const getGenerators = createSelector(\n state,\n state => {\n const languageKeys = state\n .get(\"languages\")\n const generators = state\n .get(\"generators\", Map())\n if(!languageKeys || languageKeys.isEmpty()) {\n return generators\n }\n return generators\n .filter((v, key) => languageKeys.includes(key))\n }\n)\n\nexport const getSnippetGenerators = (state) => ({ fn }) => {\n const getGenFn = (key) => fn[`requestSnippetGenerator_${key}`]\n return getGenerators(state)\n .map((gen, key) => {\n const genFn = getGenFn(key)\n if(typeof genFn !== \"function\") {\n return null\n }\n\n return gen.set(\"fn\", genFn)\n })\n .filter(v => v)\n}\n\nexport const getActiveLanguage = createSelector(\n state,\n state => state\n .get(\"activeLanguage\")\n)\n\nexport const getDefaultExpanded = createSelector(\n state,\n state => state\n .get(\"defaultExpanded\")\n)\n","import PropTypes from \"prop-types\"\nimport React, { Component } from \"react\"\n\nimport { componentDidCatch } from \"../fn\"\nimport Fallback from \"./fallback\"\n\nexport class ErrorBoundary extends Component {\n static getDerivedStateFromError(error) {\n return { hasError: true, error }\n }\n\n constructor(...args) {\n super(...args)\n this.state = { hasError: false, error: null }\n }\n\n componentDidCatch(error, errorInfo) {\n this.props.fn.componentDidCatch(error, errorInfo)\n }\n\n render() {\n const { getComponent, targetName, children } = this.props\n\n if (this.state.hasError) {\n const FallbackComponent = getComponent(\"Fallback\")\n return \n }\n\n return children\n }\n}\nErrorBoundary.propTypes = {\n targetName: PropTypes.string,\n getComponent: PropTypes.func,\n fn: PropTypes.object,\n children: PropTypes.oneOfType([\n PropTypes.arrayOf(PropTypes.node),\n PropTypes.node,\n ])\n}\nErrorBoundary.defaultProps = {\n targetName: \"this component\",\n getComponent: () => Fallback,\n fn: {\n componentDidCatch,\n },\n children: null,\n}\n\nexport default ErrorBoundary\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\n\nconst Fallback = ({ name }) => (\n
    \n 😱 Could not render { name === \"t\" ? \"this component\" : name }, see the console.\n
    \n)\nFallback.propTypes = {\n name: PropTypes.string.isRequired,\n}\n\nexport default Fallback\n","import React, { Component } from \"react\"\n\nexport const componentDidCatch = console.error\n\nconst isClassComponent = component => component.prototype && component.prototype.isReactComponent\n\nexport const withErrorBoundary = (getSystem) => (WrappedComponent) => {\n const { getComponent, fn } = getSystem()\n const ErrorBoundary = getComponent(\"ErrorBoundary\")\n const targetName = fn.getDisplayName(WrappedComponent)\n\n class WithErrorBoundary extends Component {\n render() {\n return (\n \n \n \n )\n }\n }\n WithErrorBoundary.displayName = `WithErrorBoundary(${targetName})`\n if (isClassComponent(WrappedComponent)) {\n /**\n * We need to handle case of class components defining a `mapStateToProps` public method.\n * Components with `mapStateToProps` public method cannot be wrapped.\n */\n WithErrorBoundary.prototype.mapStateToProps = WrappedComponent.prototype.mapStateToProps\n }\n\n return WithErrorBoundary\n}\n\n","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_instance_fill_07ef3114__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_lodash_zipObject_c74f1c14__[\"default\"] });","import zipObject from \"lodash/zipObject\"\n\nimport ErrorBoundary from \"./components/error-boundary\"\nimport Fallback from \"./components/fallback\"\nimport { componentDidCatch, withErrorBoundary } from \"./fn\"\n\nconst safeRenderPlugin = ({componentList = [], fullOverride = false} = {}) => ({ getSystem }) => {\n const defaultComponentList = [\n \"App\",\n \"BaseLayout\",\n \"VersionPragmaFilter\",\n \"InfoContainer\",\n \"ServersContainer\",\n \"SchemesContainer\",\n \"AuthorizeBtnContainer\",\n \"FilterContainer\",\n \"Operations\",\n \"OperationContainer\",\n \"parameters\",\n \"responses\",\n \"OperationServers\",\n \"Models\",\n \"ModelWrapper\",\n ]\n const mergedComponentList = fullOverride ? componentList : [...defaultComponentList, ...componentList]\n const wrapFactory = (Original, { fn }) => fn.withErrorBoundary(Original)\n const wrapComponents = zipObject(mergedComponentList, Array(mergedComponentList.length).fill(wrapFactory))\n\n return {\n fn: {\n componentDidCatch,\n withErrorBoundary: withErrorBoundary(getSystem),\n },\n components: {\n ErrorBoundary,\n Fallback,\n },\n wrapComponents,\n }\n}\n\nexport default safeRenderPlugin\n","/**\n * @prettier\n */\nimport some from \"lodash/some\"\n\nconst shouldStringifyTypesConfig = [\n {\n when: /json/,\n shouldStringifyTypes: [\"string\"],\n },\n]\nconst defaultStringifyTypes = [\"object\"]\nconst makeGetJsonSampleSchema =\n (getSystem) => (schema, config, contentType, exampleOverride) => {\n const { fn } = getSystem()\n const res = fn.memoizedSampleFromSchema(schema, config, exampleOverride)\n const resType = typeof res\n\n const typesToStringify = shouldStringifyTypesConfig.reduce(\n (types, nextConfig) =>\n nextConfig.when.test(contentType)\n ? [...types, ...nextConfig.shouldStringifyTypes]\n : types,\n defaultStringifyTypes\n )\n\n return some(typesToStringify, (x) => x === resType)\n ? JSON.stringify(res, null, 2)\n : res\n }\n\nexport default makeGetJsonSampleSchema\n","/**\n * @prettier\n */\nconst makeGetSampleSchema =\n (getSystem) =>\n (schema, contentType = \"\", config = {}, exampleOverride = undefined) => {\n const { fn } = getSystem()\n\n if (typeof schema?.toJS === \"function\") {\n schema = schema.toJS()\n }\n if (typeof exampleOverride?.toJS === \"function\") {\n exampleOverride = exampleOverride.toJS()\n }\n\n if (/xml/.test(contentType)) {\n return fn.getXmlSampleSchema(schema, config, exampleOverride)\n }\n if (/(yaml|yml)/.test(contentType)) {\n return fn.getYamlSampleSchema(\n schema,\n config,\n contentType,\n exampleOverride\n )\n }\n return fn.getJsonSampleSchema(schema, config, contentType, exampleOverride)\n }\n\nexport default makeGetSampleSchema\n","/**\n * @prettier\n */\nconst makeGetXmlSampleSchema =\n (getSystem) => (schema, config, exampleOverride) => {\n const { fn } = getSystem()\n\n if (schema && !schema.xml) {\n schema.xml = {}\n }\n if (schema && !schema.xml.name) {\n if (\n !schema.$$ref &&\n (schema.type ||\n schema.items ||\n schema.properties ||\n schema.additionalProperties)\n ) {\n // eslint-disable-next-line quotes\n return '\\n'\n }\n if (schema.$$ref) {\n let match = schema.$$ref.match(/\\S*\\/(\\S+)$/)\n schema.xml.name = match[1]\n }\n }\n\n return fn.memoizedCreateXMLExample(schema, config, exampleOverride)\n }\n\nexport default makeGetXmlSampleSchema\n","/**\n * @prettier\n */\nimport YAML, { JSON_SCHEMA } from \"js-yaml\"\n\nconst makeGetYamlSampleSchema =\n (getSystem) => (schema, config, contentType, exampleOverride) => {\n const { fn } = getSystem()\n const jsonExample = fn.getJsonSampleSchema(\n schema,\n config,\n contentType,\n exampleOverride\n )\n let yamlString\n try {\n yamlString = YAML.dump(\n YAML.load(jsonExample),\n {\n lineWidth: -1, // don't generate line folds\n },\n { schema: JSON_SCHEMA }\n )\n if (yamlString[yamlString.length - 1] === \"\\n\") {\n yamlString = yamlString.slice(0, yamlString.length - 1)\n }\n } catch (e) {\n console.error(e)\n return \"error: could not generate yaml example\"\n }\n return yamlString.replace(/\\t/g, \" \")\n }\n\nexport default makeGetYamlSampleSchema\n","import XML from \"xml\"\nimport RandExp from \"randexp\"\nimport isEmpty from \"lodash/isEmpty\"\nimport { objectify, isFunc, normalizeArray, deeplyStripKey } from \"core/utils\"\n\nimport memoizeN from \"../../../../helpers/memoizeN\"\n\nconst generateStringFromRegex = (pattern) => {\n try {\n const randexp = new RandExp(pattern)\n return randexp.gen()\n } catch (e) {\n // Invalid regex should not cause a crash (regex syntax varies across languages)\n return \"string\"\n }\n}\n\nconst primitives = {\n \"string\": (schema) => schema.pattern ? generateStringFromRegex(schema.pattern) : \"string\",\n \"string_email\": () => \"user@example.com\",\n \"string_date-time\": () => new Date().toISOString(),\n \"string_date\": () => new Date().toISOString().substring(0, 10),\n \"string_uuid\": () => \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n \"string_hostname\": () => \"example.com\",\n \"string_ipv4\": () => \"198.51.100.42\",\n \"string_ipv6\": () => \"2001:0db8:5b96:0000:0000:426f:8e17:642a\",\n \"number\": () => 0,\n \"number_float\": () => 0.0,\n \"integer\": () => 0,\n \"boolean\": (schema) => typeof schema.default === \"boolean\" ? schema.default : true\n}\n\nconst primitive = (schema) => {\n schema = objectify(schema)\n let { type, format } = schema\n\n let fn = primitives[`${type}_${format}`] || primitives[type]\n\n if(isFunc(fn))\n return fn(schema)\n\n return \"Unknown Type: \" + schema.type\n}\n\n// do a couple of quick sanity tests to ensure the value\n// looks like a $$ref that swagger-client generates.\nconst sanitizeRef = (value) => deeplyStripKey(value, \"$$ref\", (val) =>\n typeof val === \"string\" && val.indexOf(\"#\") > -1)\n\nconst objectContracts = [\"maxProperties\", \"minProperties\"]\nconst arrayContracts = [\"minItems\", \"maxItems\"]\nconst numberContracts = [\n \"minimum\",\n \"maximum\",\n \"exclusiveMinimum\",\n \"exclusiveMaximum\"\n]\nconst stringContracts = [\"minLength\", \"maxLength\"]\n\nconst liftSampleHelper = (oldSchema, target, config = {}) => {\n const setIfNotDefinedInTarget = (key) => {\n if(target[key] === undefined && oldSchema[key] !== undefined) {\n target[key] = oldSchema[key]\n }\n }\n\n [\n \"example\",\n \"default\",\n \"enum\",\n \"xml\",\n \"type\",\n ...objectContracts,\n ...arrayContracts,\n ...numberContracts,\n ...stringContracts,\n ].forEach(key => setIfNotDefinedInTarget(key))\n\n if(oldSchema.required !== undefined && Array.isArray(oldSchema.required)) {\n if(target.required === undefined || !target.required.length) {\n target.required = []\n }\n oldSchema.required.forEach(key => {\n if(target.required.includes(key)) {\n return\n }\n target.required.push(key)\n })\n }\n if(oldSchema.properties) {\n if(!target.properties) {\n target.properties = {}\n }\n let props = objectify(oldSchema.properties)\n for (let propName in props) {\n if (!Object.prototype.hasOwnProperty.call(props, propName)) {\n continue\n }\n if ( props[propName] && props[propName].deprecated ) {\n continue\n }\n if ( props[propName] && props[propName].readOnly && !config.includeReadOnly ) {\n continue\n }\n if ( props[propName] && props[propName].writeOnly && !config.includeWriteOnly ) {\n continue\n }\n if(!target.properties[propName]) {\n target.properties[propName] = props[propName]\n if(!oldSchema.required && Array.isArray(oldSchema.required) && oldSchema.required.indexOf(propName) !== -1) {\n if(!target.required) {\n target.required = [propName]\n } else {\n target.required.push(propName)\n }\n }\n }\n }\n }\n if(oldSchema.items) {\n if(!target.items) {\n target.items = {}\n }\n target.items = liftSampleHelper(oldSchema.items, target.items, config)\n }\n\n return target\n}\n\nexport const sampleFromSchemaGeneric = (schema, config={}, exampleOverride = undefined, respectXML = false) => {\n if(schema && isFunc(schema.toJS))\n schema = schema.toJS()\n let usePlainValue = exampleOverride !== undefined || schema && schema.example !== undefined || schema && schema.default !== undefined\n // first check if there is the need of combining this schema with others required by allOf\n const hasOneOf = !usePlainValue && schema && schema.oneOf && schema.oneOf.length > 0\n const hasAnyOf = !usePlainValue && schema && schema.anyOf && schema.anyOf.length > 0\n if(!usePlainValue && (hasOneOf || hasAnyOf)) {\n const schemaToAdd = objectify(hasOneOf\n ? schema.oneOf[0]\n : schema.anyOf[0]\n )\n liftSampleHelper(schemaToAdd, schema, config)\n if(!schema.xml && schemaToAdd.xml) {\n schema.xml = schemaToAdd.xml\n }\n if(schema.example !== undefined && schemaToAdd.example !== undefined) {\n usePlainValue = true\n } else if(schemaToAdd.properties) {\n if(!schema.properties) {\n schema.properties = {}\n }\n let props = objectify(schemaToAdd.properties)\n for (let propName in props) {\n if (!Object.prototype.hasOwnProperty.call(props, propName)) {\n continue\n }\n if ( props[propName] && props[propName].deprecated ) {\n continue\n }\n if ( props[propName] && props[propName].readOnly && !config.includeReadOnly ) {\n continue\n }\n if ( props[propName] && props[propName].writeOnly && !config.includeWriteOnly ) {\n continue\n }\n if(!schema.properties[propName]) {\n schema.properties[propName] = props[propName]\n if(!schemaToAdd.required && Array.isArray(schemaToAdd.required) && schemaToAdd.required.indexOf(propName) !== -1) {\n if(!schema.required) {\n schema.required = [propName]\n } else {\n schema.required.push(propName)\n }\n }\n }\n }\n }\n }\n const _attr = {}\n let { xml, type, example, properties, additionalProperties, items } = schema || {}\n let { includeReadOnly, includeWriteOnly } = config\n xml = xml || {}\n let { name, prefix, namespace } = xml\n let displayName\n let res = {}\n\n // set xml naming and attributes\n if(respectXML) {\n name = name || \"notagname\"\n // add prefix to name if exists\n displayName = (prefix ? prefix + \":\" : \"\") + name\n if ( namespace ) {\n //add prefix to namespace if exists\n let namespacePrefix = prefix ? ( \"xmlns:\" + prefix ) : \"xmlns\"\n _attr[namespacePrefix] = namespace\n }\n }\n\n // init xml default response sample obj\n if(respectXML) {\n res[displayName] = []\n }\n\n const schemaHasAny = (keys) => keys.some(key => Object.prototype.hasOwnProperty.call(schema, key))\n // try recover missing type\n if(schema && !type) {\n if(properties || additionalProperties || schemaHasAny(objectContracts)) {\n type = \"object\"\n } else if(items || schemaHasAny(arrayContracts)) {\n type = \"array\"\n } else if(schemaHasAny(numberContracts)) {\n type = \"number\"\n schema.type = \"number\"\n } else if(!usePlainValue && !schema.enum){\n // implicit cover schemaHasAny(stringContracts) or A schema without a type matches any data type is:\n // components:\n // schemas:\n // AnyValue:\n // anyOf:\n // - type: string\n // - type: number\n // - type: integer\n // - type: boolean\n // - type: array\n // items: {}\n // - type: object\n //\n // which would resolve to type: string\n type = \"string\"\n schema.type = \"string\"\n }\n }\n\n const handleMinMaxItems = (sampleArray) => {\n if (schema?.maxItems !== null && schema?.maxItems !== undefined) {\n sampleArray = sampleArray.slice(0, schema?.maxItems)\n }\n if (schema?.minItems !== null && schema?.minItems !== undefined) {\n let i = 0\n while (sampleArray.length < schema?.minItems) {\n sampleArray.push(sampleArray[i++ % sampleArray.length])\n }\n }\n return sampleArray\n }\n\n // add to result helper init for xml or json\n const props = objectify(properties)\n let addPropertyToResult\n let propertyAddedCounter = 0\n\n const hasExceededMaxProperties = () => schema\n && schema.maxProperties !== null && schema.maxProperties !== undefined\n && propertyAddedCounter >= schema.maxProperties\n\n const requiredPropertiesToAdd = () => {\n if(!schema || !schema.required) {\n return 0\n }\n let addedCount = 0\n if(respectXML) {\n schema.required.forEach(key => addedCount +=\n res[key] === undefined\n ? 0\n : 1\n )\n } else {\n schema.required.forEach(key => addedCount +=\n res[displayName]?.find(x => x[key] !== undefined) === undefined\n ? 0\n : 1\n )\n }\n return schema.required.length - addedCount\n }\n\n const isOptionalProperty = (propName) => {\n if(!schema || !schema.required || !schema.required.length) {\n return true\n }\n return !schema.required.includes(propName)\n }\n\n const canAddProperty = (propName) => {\n if(!schema || schema.maxProperties === null || schema.maxProperties === undefined) {\n return true\n }\n if(hasExceededMaxProperties()) {\n return false\n }\n if(!isOptionalProperty(propName)) {\n return true\n }\n return (schema.maxProperties - propertyAddedCounter - requiredPropertiesToAdd()) > 0\n }\n\n if(respectXML) {\n addPropertyToResult = (propName, overrideE = undefined) => {\n if(schema && props[propName]) {\n // case it is an xml attribute\n props[propName].xml = props[propName].xml || {}\n\n if (props[propName].xml.attribute) {\n const enumAttrVal = Array.isArray(props[propName].enum)\n ? props[propName].enum[0]\n : undefined\n const attrExample = props[propName].example\n const attrDefault = props[propName].default\n\n if(attrExample !== undefined) {\n _attr[props[propName].xml.name || propName] = attrExample\n } else if(attrDefault !== undefined) {\n _attr[props[propName].xml.name || propName] = attrDefault\n } else if(enumAttrVal !== undefined) {\n _attr[props[propName].xml.name || propName] = enumAttrVal\n } else {\n _attr[props[propName].xml.name || propName] = primitive(props[propName])\n }\n\n return\n }\n props[propName].xml.name = props[propName].xml.name || propName\n } else if(!props[propName] && additionalProperties !== false) {\n // case only additionalProperty that is not defined in schema\n props[propName] = {\n xml: {\n name: propName\n }\n }\n }\n\n let t = sampleFromSchemaGeneric(schema && props[propName] || undefined, config, overrideE, respectXML)\n if(!canAddProperty(propName)) {\n return\n }\n\n propertyAddedCounter++\n if (Array.isArray(t)) {\n res[displayName] = res[displayName].concat(t)\n } else {\n res[displayName].push(t)\n }\n }\n } else {\n addPropertyToResult = (propName, overrideE) => {\n if(!canAddProperty(propName)) {\n return\n }\n if(Object.prototype.hasOwnProperty.call(schema, \"discriminator\") &&\n schema.discriminator &&\n Object.prototype.hasOwnProperty.call(schema.discriminator, \"mapping\") &&\n schema.discriminator.mapping &&\n Object.prototype.hasOwnProperty.call(schema, \"$$ref\") &&\n schema.$$ref &&\n schema.discriminator.propertyName === propName) {\n for (let pair in schema.discriminator.mapping){\n if (schema.$$ref.search(schema.discriminator.mapping[pair]) !== -1) {\n res[propName] = pair\n break\n }\n }\n } else {\n res[propName] = sampleFromSchemaGeneric(props[propName], config, overrideE, respectXML)\n }\n propertyAddedCounter++\n }\n }\n\n // check for plain value and if found use it to generate sample from it\n if(usePlainValue) {\n let sample\n if(exampleOverride !== undefined) {\n sample = sanitizeRef(exampleOverride)\n } else if(example !== undefined) {\n sample = sanitizeRef(example)\n } else {\n sample = sanitizeRef(schema.default)\n }\n\n // if json just return\n if(!respectXML) {\n // spacial case yaml parser can not know about\n if(typeof sample === \"number\" && type === \"string\") {\n return `${sample}`\n }\n // return if sample does not need any parsing\n if(typeof sample !== \"string\" || type === \"string\") {\n return sample\n }\n // check if sample is parsable or just a plain string\n try {\n return JSON.parse(sample)\n } catch(e) {\n // sample is just plain string return it\n return sample\n }\n }\n\n // recover missing type\n if(!schema) {\n type = Array.isArray(sample) ? \"array\" : typeof sample\n }\n\n // generate xml sample recursively for array case\n if(type === \"array\") {\n if (!Array.isArray(sample)) {\n if(typeof sample === \"string\") {\n return sample\n }\n sample = [sample]\n }\n const itemSchema = schema\n ? schema.items\n : undefined\n if(itemSchema) {\n itemSchema.xml = itemSchema.xml || xml || {}\n itemSchema.xml.name = itemSchema.xml.name || xml.name\n }\n let itemSamples = sample\n .map(s => sampleFromSchemaGeneric(itemSchema, config, s, respectXML))\n itemSamples = handleMinMaxItems(itemSamples)\n if(xml.wrapped) {\n res[displayName] = itemSamples\n if (!isEmpty(_attr)) {\n res[displayName].push({_attr: _attr})\n }\n }\n else {\n res = itemSamples\n }\n return res\n }\n\n // generate xml sample recursively for object case\n if(type === \"object\") {\n // case literal example\n if(typeof sample === \"string\") {\n return sample\n }\n for (let propName in sample) {\n if (!Object.prototype.hasOwnProperty.call(sample, propName)) {\n continue\n }\n if (schema && props[propName] && props[propName].readOnly && !includeReadOnly) {\n continue\n }\n if (schema && props[propName] && props[propName].writeOnly && !includeWriteOnly) {\n continue\n }\n if (schema && props[propName] && props[propName].xml && props[propName].xml.attribute) {\n _attr[props[propName].xml.name || propName] = sample[propName]\n continue\n }\n addPropertyToResult(propName, sample[propName])\n }\n if (!isEmpty(_attr)) {\n res[displayName].push({_attr: _attr})\n }\n\n return res\n }\n\n res[displayName] = !isEmpty(_attr) ? [{_attr: _attr}, sample] : sample\n return res\n }\n\n // use schema to generate sample\n\n if(type === \"object\") {\n for (let propName in props) {\n if (!Object.prototype.hasOwnProperty.call(props, propName)) {\n continue\n }\n if ( props[propName] && props[propName].deprecated ) {\n continue\n }\n if ( props[propName] && props[propName].readOnly && !includeReadOnly ) {\n continue\n }\n if ( props[propName] && props[propName].writeOnly && !includeWriteOnly ) {\n continue\n }\n addPropertyToResult(propName)\n }\n if (respectXML && _attr) {\n res[displayName].push({_attr: _attr})\n }\n\n if(hasExceededMaxProperties()) {\n return res\n }\n\n if ( additionalProperties === true ) {\n if(respectXML) {\n res[displayName].push({additionalProp: \"Anything can be here\"})\n } else {\n res.additionalProp1 = {}\n }\n propertyAddedCounter++\n } else if ( additionalProperties ) {\n const additionalProps = objectify(additionalProperties)\n const additionalPropSample = sampleFromSchemaGeneric(additionalProps, config, undefined, respectXML)\n\n if(respectXML && additionalProps.xml && additionalProps.xml.name && additionalProps.xml.name !== \"notagname\")\n {\n res[displayName].push(additionalPropSample)\n } else {\n const toGenerateCount = schema.minProperties !== null && schema.minProperties !== undefined && propertyAddedCounter < schema.minProperties\n ? schema.minProperties - propertyAddedCounter\n : 3\n for (let i = 1; i <= toGenerateCount; i++) {\n if(hasExceededMaxProperties()) {\n return res\n }\n if(respectXML) {\n const temp = {}\n temp[\"additionalProp\" + i] = additionalPropSample[\"notagname\"]\n res[displayName].push(temp)\n } else {\n res[\"additionalProp\" + i] = additionalPropSample\n }\n propertyAddedCounter++\n }\n }\n }\n return res\n }\n\n if(type === \"array\") {\n if (!items) {\n return\n }\n\n let sampleArray\n if(respectXML) {\n items.xml = items.xml || schema?.xml || {}\n items.xml.name = items.xml.name || xml.name\n }\n\n if(Array.isArray(items.anyOf)) {\n sampleArray = items.anyOf.map(i => sampleFromSchemaGeneric(liftSampleHelper(items, i, config), config, undefined, respectXML))\n } else if(Array.isArray(items.oneOf)) {\n sampleArray = items.oneOf.map(i => sampleFromSchemaGeneric(liftSampleHelper(items, i, config), config, undefined, respectXML))\n } else if(!respectXML || respectXML && xml.wrapped) {\n sampleArray = [sampleFromSchemaGeneric(items, config, undefined, respectXML)]\n } else {\n return sampleFromSchemaGeneric(items, config, undefined, respectXML)\n }\n sampleArray = handleMinMaxItems(sampleArray)\n if(respectXML && xml.wrapped) {\n res[displayName] = sampleArray\n if (!isEmpty(_attr)) {\n res[displayName].push({_attr: _attr})\n }\n return res\n }\n return sampleArray\n }\n\n let value\n if (schema && Array.isArray(schema.enum)) {\n //display enum first value\n value = normalizeArray(schema.enum)[0]\n } else if(schema) {\n // display schema default\n value = primitive(schema)\n if(typeof value === \"number\") {\n let min = schema.minimum\n if(min !== undefined && min !== null) {\n if(schema.exclusiveMinimum) {\n min++\n }\n value = min\n }\n let max = schema.maximum\n if(max !== undefined && max !== null) {\n if(schema.exclusiveMaximum) {\n max--\n }\n value = max\n }\n }\n if(typeof value === \"string\") {\n if (schema.maxLength !== null && schema.maxLength !== undefined) {\n value = value.slice(0, schema.maxLength)\n }\n if (schema.minLength !== null && schema.minLength !== undefined) {\n let i = 0\n while (value.length < schema.minLength) {\n value += value[i++ % value.length]\n }\n }\n }\n } else {\n return\n }\n if (type === \"file\") {\n return\n }\n\n if(respectXML) {\n res[displayName] = !isEmpty(_attr) ? [{_attr: _attr}, value] : value\n return res\n }\n\n return value\n}\n\nexport const inferSchema = (thing) => {\n if(thing.schema)\n thing = thing.schema\n\n if(thing.properties) {\n thing.type = \"object\"\n }\n\n return thing // Hopefully this will have something schema like in it... `type` for example\n}\n\nexport const createXMLExample = (schema, config, o) => {\n const json = sampleFromSchemaGeneric(schema, config, o, true)\n if (!json) { return }\n if(typeof json === \"string\") {\n return json\n }\n return XML(json, { declaration: true, indent: \"\\t\" })\n}\n\nexport const sampleFromSchema = (schema, config, o) =>\n sampleFromSchemaGeneric(schema, config, o, false)\n\nconst resolver = (arg1, arg2, arg3) => [arg1, JSON.stringify(arg2), JSON.stringify(arg3)]\n\nexport const memoizedCreateXMLExample = memoizeN(createXMLExample, resolver)\n\nexport const memoizedSampleFromSchema = memoizeN(sampleFromSchema, resolver)\n","/**\n * @prettier\n */\nimport {\n sampleFromSchema,\n inferSchema,\n sampleFromSchemaGeneric,\n createXMLExample,\n memoizedCreateXMLExample,\n memoizedSampleFromSchema,\n} from \"./fn/index\"\nimport makeGetJsonSampleSchema from \"./fn/get-json-sample-schema\"\nimport makeGetYamlSampleSchema from \"./fn/get-yaml-sample-schema\"\nimport makeGetXmlSampleSchema from \"./fn/get-xml-sample-schema\"\nimport makeGetSampleSchema from \"./fn/get-sample-schema\"\n\nconst SamplesPlugin = ({ getSystem }) => ({\n fn: {\n inferSchema,\n sampleFromSchema,\n sampleFromSchemaGeneric,\n createXMLExample,\n memoizedSampleFromSchema,\n memoizedCreateXMLExample,\n getJsonSampleSchema: makeGetJsonSampleSchema(getSystem),\n getYamlSampleSchema: makeGetYamlSampleSchema(getSystem),\n getXmlSampleSchema: makeGetXmlSampleSchema(getSystem),\n getSampleSchema: makeGetSampleSchema(getSystem),\n },\n})\n\nexport default SamplesPlugin\n","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_promise_047dc8e8__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_date_now_1bf78713__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_lodash_isString_e6fa8a5b__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_lodash_debounce_3540babe__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_lodash_set_b4b15ee5__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_lodash_fp_assocPath_f9d64e33__[\"default\"] });","import YAML, { JSON_SCHEMA } from \"js-yaml\"\nimport { Map } from \"immutable\"\nimport parseUrl from \"url-parse\"\nimport { serializeError } from \"serialize-error\"\nimport isString from \"lodash/isString\"\nimport debounce from \"lodash/debounce\"\nimport set from \"lodash/set\"\nimport assocPath from \"lodash/fp/assocPath\"\n\nimport { paramToValue, isEmptyValue } from \"core/utils\"\n\n// Actions conform to FSA (flux-standard-actions)\n// {type: string,payload: Any|Error, meta: obj, error: bool}\n\nexport const UPDATE_SPEC = \"spec_update_spec\"\nexport const UPDATE_URL = \"spec_update_url\"\nexport const UPDATE_JSON = \"spec_update_json\"\nexport const UPDATE_PARAM = \"spec_update_param\"\nexport const UPDATE_EMPTY_PARAM_INCLUSION = \"spec_update_empty_param_inclusion\"\nexport const VALIDATE_PARAMS = \"spec_validate_param\"\nexport const SET_RESPONSE = \"spec_set_response\"\nexport const SET_REQUEST = \"spec_set_request\"\nexport const SET_MUTATED_REQUEST = \"spec_set_mutated_request\"\nexport const LOG_REQUEST = \"spec_log_request\"\nexport const CLEAR_RESPONSE = \"spec_clear_response\"\nexport const CLEAR_REQUEST = \"spec_clear_request\"\nexport const CLEAR_VALIDATE_PARAMS = \"spec_clear_validate_param\"\nexport const UPDATE_OPERATION_META_VALUE = \"spec_update_operation_meta_value\"\nexport const UPDATE_RESOLVED = \"spec_update_resolved\"\nexport const UPDATE_RESOLVED_SUBTREE = \"spec_update_resolved_subtree\"\nexport const SET_SCHEME = \"set_scheme\"\n\nconst toStr = (str) => isString(str) ? str : \"\"\n\nexport function updateSpec(spec) {\n const cleanSpec = (toStr(spec)).replace(/\\t/g, \" \")\n if(typeof spec === \"string\") {\n return {\n type: UPDATE_SPEC,\n payload: cleanSpec\n }\n }\n}\n\nexport function updateResolved(spec) {\n return {\n type: UPDATE_RESOLVED,\n payload: spec\n }\n}\n\nexport function updateUrl(url) {\n return {type: UPDATE_URL, payload: url}\n}\n\nexport function updateJsonSpec(json) {\n return {type: UPDATE_JSON, payload: json}\n}\n\nexport const parseToJson = (str) => ({specActions, specSelectors, errActions}) => {\n let { specStr } = specSelectors\n\n let json = null\n try {\n str = str || specStr()\n errActions.clear({ source: \"parser\" })\n json = YAML.load(str, { schema: JSON_SCHEMA })\n } catch(e) {\n // TODO: push error to state\n console.error(e)\n return errActions.newSpecErr({\n source: \"parser\",\n level: \"error\",\n message: e.reason,\n line: e.mark && e.mark.line ? e.mark.line + 1 : undefined\n })\n }\n if(json && typeof json === \"object\") {\n return specActions.updateJsonSpec(json)\n }\n return {}\n}\n\nlet hasWarnedAboutResolveSpecDeprecation = false\n\nexport const resolveSpec = (json, url) => ({specActions, specSelectors, errActions, fn: { fetch, resolve, AST = {} }, getConfigs}) => {\n if(!hasWarnedAboutResolveSpecDeprecation) {\n console.warn(`specActions.resolveSpec is deprecated since v3.10.0 and will be removed in v4.0.0; use requestResolvedSubtree instead!`)\n hasWarnedAboutResolveSpecDeprecation = true\n }\n\n const {\n modelPropertyMacro,\n parameterMacro,\n requestInterceptor,\n responseInterceptor\n } = getConfigs()\n\n if(typeof(json) === \"undefined\") {\n json = specSelectors.specJson()\n }\n if(typeof(url) === \"undefined\") {\n url = specSelectors.url()\n }\n\n let getLineNumberForPath = AST.getLineNumberForPath ? AST.getLineNumberForPath : () => undefined\n\n let specStr = specSelectors.specStr()\n\n return resolve({\n fetch,\n spec: json,\n baseDoc: url,\n modelPropertyMacro,\n parameterMacro,\n requestInterceptor,\n responseInterceptor\n }).then( ({spec, errors}) => {\n errActions.clear({\n type: \"thrown\"\n })\n if(Array.isArray(errors) && errors.length > 0) {\n let preparedErrors = errors\n .map(err => {\n console.error(err)\n err.line = err.fullPath ? getLineNumberForPath(specStr, err.fullPath) : null\n err.path = err.fullPath ? err.fullPath.join(\".\") : null\n err.level = \"error\"\n err.type = \"thrown\"\n err.source = \"resolver\"\n Object.defineProperty(err, \"message\", { enumerable: true, value: err.message })\n return err\n })\n errActions.newThrownErrBatch(preparedErrors)\n }\n\n return specActions.updateResolved(spec)\n })\n}\n\nlet requestBatch = []\n\nconst debResolveSubtrees = debounce(async () => {\n const system = requestBatch.system // Just a reference to the \"latest\" system\n\n if(!system) {\n console.error(\"debResolveSubtrees: don't have a system to operate on, aborting.\")\n return\n }\n const {\n errActions,\n errSelectors,\n fn: {\n resolveSubtree,\n fetch,\n AST = {}\n },\n specSelectors,\n specActions,\n } = system\n\n if(!resolveSubtree) {\n console.error(\"Error: Swagger-Client did not provide a `resolveSubtree` method, doing nothing.\")\n return\n }\n\n let getLineNumberForPath = AST.getLineNumberForPath ? AST.getLineNumberForPath : () => undefined\n\n const specStr = specSelectors.specStr()\n\n const {\n modelPropertyMacro,\n parameterMacro,\n requestInterceptor,\n responseInterceptor\n } = system.getConfigs()\n\n try {\n var batchResult = await requestBatch.reduce(async (prev, path) => {\n let { resultMap, specWithCurrentSubtrees } = await prev\n const { errors, spec } = await resolveSubtree(specWithCurrentSubtrees, path, {\n baseDoc: specSelectors.url(),\n modelPropertyMacro,\n parameterMacro,\n requestInterceptor,\n responseInterceptor\n })\n\n if(errSelectors.allErrors().size) {\n errActions.clearBy(err => {\n // keep if...\n return err.get(\"type\") !== \"thrown\" // it's not a thrown error\n || err.get(\"source\") !== \"resolver\" // it's not a resolver error\n || !err.get(\"fullPath\").every((key, i) => key === path[i] || path[i] === undefined) // it's not within the path we're resolving\n })\n }\n\n if(Array.isArray(errors) && errors.length > 0) {\n let preparedErrors = errors\n .map(err => {\n err.line = err.fullPath ? getLineNumberForPath(specStr, err.fullPath) : null\n err.path = err.fullPath ? err.fullPath.join(\".\") : null\n err.level = \"error\"\n err.type = \"thrown\"\n err.source = \"resolver\"\n Object.defineProperty(err, \"message\", { enumerable: true, value: err.message })\n return err\n })\n errActions.newThrownErrBatch(preparedErrors)\n }\n\n if (spec && specSelectors.isOAS3() && path[0] === \"components\" && path[1] === \"securitySchemes\") {\n // Resolve OIDC URLs if present\n await Promise.all(Object.values(spec)\n .filter((scheme) => scheme.type === \"openIdConnect\")\n .map(async (oidcScheme) => {\n const req = {\n url: oidcScheme.openIdConnectUrl,\n requestInterceptor: requestInterceptor,\n responseInterceptor: responseInterceptor\n }\n try {\n const res = await fetch(req)\n if (res instanceof Error || res.status >= 400) {\n console.error(res.statusText + \" \" + req.url)\n } else {\n oidcScheme.openIdConnectData = JSON.parse(res.text)\n }\n } catch (e) {\n console.error(e)\n }\n }))\n }\n set(resultMap, path, spec)\n specWithCurrentSubtrees = assocPath(path, spec, specWithCurrentSubtrees)\n\n return {\n resultMap,\n specWithCurrentSubtrees\n }\n }, Promise.resolve({\n resultMap: (specSelectors.specResolvedSubtree([]) || Map()).toJS(),\n specWithCurrentSubtrees: specSelectors.specJS()\n }))\n\n delete requestBatch.system\n requestBatch = [] // Clear stack\n } catch(e) {\n console.error(e)\n }\n\n specActions.updateResolvedSubtree([], batchResult.resultMap)\n}, 35)\n\nexport const requestResolvedSubtree = path => system => {\n // poor-man's array comparison\n // if this ever inadequate, this should be rewritten to use Im.List\n const isPathAlreadyBatched = requestBatch\n .map(arr => arr.join(\"@@\"))\n .indexOf(path.join(\"@@\")) > -1\n\n if(isPathAlreadyBatched) {\n return\n }\n\n requestBatch.push(path)\n requestBatch.system = system\n debResolveSubtrees()\n}\n\nexport function changeParam( path, paramName, paramIn, value, isXml ){\n return {\n type: UPDATE_PARAM,\n payload:{ path, value, paramName, paramIn, isXml }\n }\n}\n\nexport function changeParamByIdentity( pathMethod, param, value, isXml ){\n return {\n type: UPDATE_PARAM,\n payload:{ path: pathMethod, param, value, isXml }\n }\n}\n\nexport const updateResolvedSubtree = (path, value) => {\n return {\n type: UPDATE_RESOLVED_SUBTREE,\n payload: { path, value }\n }\n}\n\nexport const invalidateResolvedSubtreeCache = () => {\n return {\n type: UPDATE_RESOLVED_SUBTREE,\n payload: {\n path: [],\n value: Map()\n }\n }\n}\n\nexport const validateParams = ( payload, isOAS3 ) =>{\n return {\n type: VALIDATE_PARAMS,\n payload:{\n pathMethod: payload,\n isOAS3\n }\n }\n}\n\nexport const updateEmptyParamInclusion = ( pathMethod, paramName, paramIn, includeEmptyValue ) =>{\n return {\n type: UPDATE_EMPTY_PARAM_INCLUSION,\n payload:{\n pathMethod,\n paramName,\n paramIn,\n includeEmptyValue\n }\n }\n}\n\nexport function clearValidateParams( payload ){\n return {\n type: CLEAR_VALIDATE_PARAMS,\n payload:{ pathMethod: payload }\n }\n}\n\nexport function changeConsumesValue(path, value) {\n return {\n type: UPDATE_OPERATION_META_VALUE,\n payload:{ path, value, key: \"consumes_value\" }\n }\n}\n\nexport function changeProducesValue(path, value) {\n return {\n type: UPDATE_OPERATION_META_VALUE,\n payload:{ path, value, key: \"produces_value\" }\n }\n}\n\nexport const setResponse = ( path, method, res ) => {\n return {\n payload: { path, method, res },\n type: SET_RESPONSE\n }\n}\n\nexport const setRequest = ( path, method, req ) => {\n return {\n payload: { path, method, req },\n type: SET_REQUEST\n }\n}\n\nexport const setMutatedRequest = ( path, method, req ) => {\n return {\n payload: { path, method, req },\n type: SET_MUTATED_REQUEST\n }\n}\n\n// This is for debugging, remove this comment if you depend on this action\nexport const logRequest = (req) => {\n return {\n payload: req,\n type: LOG_REQUEST\n }\n}\n\n// Actually fire the request via fn.execute\n// (For debugging) and ease of testing\nexport const executeRequest = (req) =>\n ({fn, specActions, specSelectors, getConfigs, oas3Selectors}) => {\n let { pathName, method, operation } = req\n let { requestInterceptor, responseInterceptor } = getConfigs()\n\n\n let op = operation.toJS()\n\n // ensure that explicitly-included params are in the request\n\n if (operation && operation.get(\"parameters\")) {\n operation.get(\"parameters\")\n .filter(param => param && param.get(\"allowEmptyValue\") === true)\n .forEach(param => {\n if (specSelectors.parameterInclusionSettingFor([pathName, method], param.get(\"name\"), param.get(\"in\"))) {\n req.parameters = req.parameters || {}\n const paramValue = paramToValue(param, req.parameters)\n\n // if the value is falsy or an empty Immutable iterable...\n if(!paramValue || (paramValue && paramValue.size === 0)) {\n // set it to empty string, so Swagger Client will treat it as\n // present but empty.\n req.parameters[param.get(\"name\")] = \"\"\n }\n }\n })\n }\n\n // if url is relative, parseUrl makes it absolute by inferring from `window.location`\n req.contextUrl = parseUrl(specSelectors.url()).toString()\n\n if(op && op.operationId) {\n req.operationId = op.operationId\n } else if(op && pathName && method) {\n req.operationId = fn.opId(op, pathName, method)\n }\n\n if(specSelectors.isOAS3()) {\n const namespace = `${pathName}:${method}`\n\n req.server = oas3Selectors.selectedServer(namespace) || oas3Selectors.selectedServer()\n\n const namespaceVariables = oas3Selectors.serverVariables({\n server: req.server,\n namespace\n }).toJS()\n const globalVariables = oas3Selectors.serverVariables({ server: req.server }).toJS()\n\n req.serverVariables = Object.keys(namespaceVariables).length ? namespaceVariables : globalVariables\n\n req.requestContentType = oas3Selectors.requestContentType(pathName, method)\n req.responseContentType = oas3Selectors.responseContentType(pathName, method) || \"*/*\"\n const requestBody = oas3Selectors.requestBodyValue(pathName, method)\n const requestBodyInclusionSetting = oas3Selectors.requestBodyInclusionSetting(pathName, method)\n\n if(requestBody && requestBody.toJS) {\n req.requestBody = requestBody\n .map(\n (val) => {\n if (Map.isMap(val)) {\n return val.get(\"value\")\n }\n return val\n }\n )\n .filter(\n (value, key) => (Array.isArray(value)\n ? value.length !== 0\n : !isEmptyValue(value)\n ) || requestBodyInclusionSetting.get(key)\n )\n .toJS()\n } else {\n req.requestBody = requestBody\n }\n }\n\n let parsedRequest = Object.assign({}, req)\n parsedRequest = fn.buildRequest(parsedRequest)\n\n specActions.setRequest(req.pathName, req.method, parsedRequest)\n\n let requestInterceptorWrapper = async (r) => {\n let mutatedRequest = await requestInterceptor.apply(this, [r])\n let parsedMutatedRequest = Object.assign({}, mutatedRequest)\n specActions.setMutatedRequest(req.pathName, req.method, parsedMutatedRequest)\n return mutatedRequest\n }\n\n req.requestInterceptor = requestInterceptorWrapper\n req.responseInterceptor = responseInterceptor\n\n // track duration of request\n const startTime = Date.now()\n\n\n return fn.execute(req)\n .then( res => {\n res.duration = Date.now() - startTime\n specActions.setResponse(req.pathName, req.method, res)\n } )\n .catch(\n err => {\n // console.error(err)\n if(err.message === \"Failed to fetch\") {\n err.name = \"\"\n err.message = \"**Failed to fetch.** \\n**Possible Reasons:** \\n - CORS \\n - Network Failure \\n - URL scheme must be \\\"http\\\" or \\\"https\\\" for CORS request.\"\n }\n specActions.setResponse(req.pathName, req.method, {\n error: true, err: serializeError(err)\n })\n }\n )\n }\n\n\n// I'm using extras as a way to inject properties into the final, `execute` method - It's not great. Anyone have a better idea? @ponelat\nexport const execute = ( { path, method, ...extras }={} ) => (system) => {\n let { fn:{fetch}, specSelectors, specActions } = system\n let spec = specSelectors.specJsonWithResolvedSubtrees().toJS()\n let scheme = specSelectors.operationScheme(path, method)\n let { requestContentType, responseContentType } = specSelectors.contentTypeValues([path, method]).toJS()\n let isXml = /xml/i.test(requestContentType)\n let parameters = specSelectors.parameterValues([path, method], isXml).toJS()\n\n return specActions.executeRequest({\n ...extras,\n fetch,\n spec,\n pathName: path,\n method, parameters,\n requestContentType,\n scheme,\n responseContentType\n })\n}\n\nexport function clearResponse (path, method) {\n return {\n type: CLEAR_RESPONSE,\n payload:{ path, method }\n }\n}\n\nexport function clearRequest (path, method) {\n return {\n type: CLEAR_REQUEST,\n payload:{ path, method }\n }\n}\n\nexport function setScheme (scheme, path, method) {\n return {\n type: SET_SCHEME,\n payload: { scheme, path, method }\n }\n}\n","import reducers from \"./reducers\"\nimport * as actions from \"./actions\"\nimport * as selectors from \"./selectors\"\nimport * as wrapActions from \"./wrap-actions\"\n\nexport default function() {\n return {\n statePlugins: {\n spec: {\n wrapActions,\n reducers,\n actions,\n selectors\n }\n }\n }\n}\n","import { fromJS, List } from \"immutable\"\nimport { fromJSOrdered, validateParam, paramToValue } from \"core/utils\"\nimport win from \"../../window\"\n\n// selector-in-reducer is suboptimal, but `operationWithMeta` is more of a helper\nimport {\n specJsonWithResolvedSubtrees,\n parameterValues,\n parameterInclusionSettingFor,\n} from \"./selectors\"\n\nimport {\n UPDATE_SPEC,\n UPDATE_URL,\n UPDATE_JSON,\n UPDATE_PARAM,\n UPDATE_EMPTY_PARAM_INCLUSION,\n VALIDATE_PARAMS,\n SET_RESPONSE,\n SET_REQUEST,\n SET_MUTATED_REQUEST,\n UPDATE_RESOLVED,\n UPDATE_RESOLVED_SUBTREE,\n UPDATE_OPERATION_META_VALUE,\n CLEAR_RESPONSE,\n CLEAR_REQUEST,\n CLEAR_VALIDATE_PARAMS,\n SET_SCHEME\n} from \"./actions\"\nimport { paramToIdentifier } from \"../../utils\"\n\nexport default {\n\n [UPDATE_SPEC]: (state, action) => {\n return (typeof action.payload === \"string\")\n ? state.set(\"spec\", action.payload)\n : state\n },\n\n [UPDATE_URL]: (state, action) => {\n return state.set(\"url\", action.payload+\"\")\n },\n\n [UPDATE_JSON]: (state, action) => {\n return state.set(\"json\", fromJSOrdered(action.payload))\n },\n\n [UPDATE_RESOLVED]: (state, action) => {\n return state.setIn([\"resolved\"], fromJSOrdered(action.payload))\n },\n\n [UPDATE_RESOLVED_SUBTREE]: (state, action) => {\n const { value, path } = action.payload\n return state.setIn([\"resolvedSubtrees\", ...path], fromJSOrdered(value))\n },\n\n [UPDATE_PARAM]: ( state, {payload} ) => {\n let { path: pathMethod, paramName, paramIn, param, value, isXml } = payload\n\n let paramKey = param ? paramToIdentifier(param) : `${paramIn}.${paramName}`\n\n const valueKey = isXml ? \"value_xml\" : \"value\"\n\n return state.setIn(\n [\"meta\", \"paths\", ...pathMethod, \"parameters\", paramKey, valueKey],\n value\n )\n },\n\n [UPDATE_EMPTY_PARAM_INCLUSION]: ( state, {payload} ) => {\n let { pathMethod, paramName, paramIn, includeEmptyValue } = payload\n\n if(!paramName || !paramIn) {\n console.warn(\"Warning: UPDATE_EMPTY_PARAM_INCLUSION could not generate a paramKey.\")\n return state\n }\n\n const paramKey = `${paramIn}.${paramName}`\n\n return state.setIn(\n [\"meta\", \"paths\", ...pathMethod, \"parameter_inclusions\", paramKey],\n includeEmptyValue\n )\n },\n\n [VALIDATE_PARAMS]: ( state, { payload: { pathMethod, isOAS3 } } ) => {\n const op = specJsonWithResolvedSubtrees(state).getIn([\"paths\", ...pathMethod])\n const paramValues = parameterValues(state, pathMethod).toJS()\n\n return state.updateIn([\"meta\", \"paths\", ...pathMethod, \"parameters\"], fromJS({}), paramMeta => {\n return op.get(\"parameters\", List()).reduce((res, param) => {\n const value = paramToValue(param, paramValues)\n const isEmptyValueIncluded = parameterInclusionSettingFor(state, pathMethod, param.get(\"name\"), param.get(\"in\"))\n const errors = validateParam(param, value, {\n bypassRequiredCheck: isEmptyValueIncluded,\n isOAS3,\n })\n return res.setIn([paramToIdentifier(param), \"errors\"], fromJS(errors))\n }, paramMeta)\n })\n },\n [CLEAR_VALIDATE_PARAMS]: ( state, { payload: { pathMethod } } ) => {\n return state.updateIn( [ \"meta\", \"paths\", ...pathMethod, \"parameters\" ], fromJS([]), parameters => {\n return parameters.map(param => param.set(\"errors\", fromJS([])))\n })\n },\n\n [SET_RESPONSE]: (state, { payload: { res, path, method } } ) =>{\n let result\n if ( res.error ) {\n result = Object.assign({\n error: true,\n name: res.err.name,\n message: res.err.message,\n statusCode: res.err.statusCode\n }, res.err.response)\n } else {\n result = res\n }\n\n // Ensure headers\n result.headers = result.headers || {}\n\n let newState = state.setIn( [ \"responses\", path, method ], fromJSOrdered(result) )\n\n // ImmutableJS messes up Blob. Needs to reset its value.\n if (win.Blob && res.data instanceof win.Blob) {\n newState = newState.setIn( [ \"responses\", path, method, \"text\" ], res.data)\n }\n return newState\n },\n\n [SET_REQUEST]: (state, { payload: { req, path, method } } ) =>{\n return state.setIn( [ \"requests\", path, method ], fromJSOrdered(req))\n },\n\n [SET_MUTATED_REQUEST]: (state, { payload: { req, path, method } } ) =>{\n return state.setIn( [ \"mutatedRequests\", path, method ], fromJSOrdered(req))\n },\n\n [UPDATE_OPERATION_META_VALUE]: (state, { payload: { path, value, key } }) => {\n // path is a pathMethod tuple... can't change the name now.\n let operationPath = [\"paths\", ...path]\n let metaPath = [\"meta\", \"paths\", ...path]\n\n if(\n !state.getIn([\"json\", ...operationPath])\n && !state.getIn([\"resolved\", ...operationPath])\n && !state.getIn([\"resolvedSubtrees\", ...operationPath])\n ) {\n // do nothing if the operation does not exist\n return state\n }\n\n return state.setIn([...metaPath, key], fromJS(value))\n },\n\n [CLEAR_RESPONSE]: (state, { payload: { path, method } } ) =>{\n return state.deleteIn( [ \"responses\", path, method ])\n },\n\n [CLEAR_REQUEST]: (state, { payload: { path, method } } ) =>{\n return state.deleteIn( [ \"requests\", path, method ])\n },\n\n [SET_SCHEME]: (state, { payload: { scheme, path, method } } ) =>{\n if ( path && method ) {\n return state.setIn( [ \"scheme\", path, method ], scheme)\n }\n\n if (!path && !method) {\n return state.setIn( [ \"scheme\", \"_defaultScheme\" ], scheme)\n }\n\n }\n\n}\n","import { createSelector } from \"reselect\"\nimport { sorters } from \"core/utils\"\nimport { fromJS, Set, Map, OrderedMap, List } from \"immutable\"\nimport { paramToIdentifier } from \"../../utils\"\n\nconst DEFAULT_TAG = \"default\"\n\nconst OPERATION_METHODS = [\n \"get\", \"put\", \"post\", \"delete\", \"options\", \"head\", \"patch\", \"trace\"\n]\n\nconst state = state => {\n return state || Map()\n}\n\nexport const lastError = createSelector(\n state,\n spec => spec.get(\"lastError\")\n)\n\nexport const url = createSelector(\n state,\n spec => spec.get(\"url\")\n)\n\nexport const specStr = createSelector(\n state,\n spec => spec.get(\"spec\") || \"\"\n)\n\nexport const specSource = createSelector(\n state,\n spec => spec.get(\"specSource\") || \"not-editor\"\n)\n\nexport const specJson = createSelector(\n state,\n spec => spec.get(\"json\", Map())\n)\n\nexport const specJS = createSelector(\n specJson,\n (spec) => spec.toJS()\n)\n\nexport const specResolved = createSelector(\n state,\n spec => spec.get(\"resolved\", Map())\n)\n\nexport const specResolvedSubtree = (state, path) => {\n return state.getIn([\"resolvedSubtrees\", ...path], undefined)\n}\n\nconst mergerFn = (oldVal, newVal) => {\n if(Map.isMap(oldVal) && Map.isMap(newVal)) {\n if(newVal.get(\"$$ref\")) {\n // resolver artifacts indicated that this key was directly resolved\n // so we should drop the old value entirely\n return newVal\n }\n\n return OrderedMap().mergeWith(\n mergerFn,\n oldVal,\n newVal\n )\n }\n\n return newVal\n}\n\nexport const specJsonWithResolvedSubtrees = createSelector(\n state,\n spec => OrderedMap().mergeWith(\n mergerFn,\n spec.get(\"json\"),\n spec.get(\"resolvedSubtrees\")\n )\n)\n\n// Default Spec ( as an object )\nexport const spec = state => {\n let res = specJson(state)\n return res\n}\n\nexport const isOAS3 = createSelector(\n // isOAS3 is stubbed out here to work around an issue with injecting more selectors\n // in the OAS3 plugin, and to ensure that the function is always available.\n // It's not perfect, but our hybrid (core+plugin code) implementation for OAS3\n // needs this. //KS\n spec,\n\t() => false\n)\n\nexport const info = createSelector(\n spec,\n\tspec => returnSelfOrNewMap(spec && spec.get(\"info\"))\n)\n\nexport const externalDocs = createSelector(\n spec,\n\tspec => returnSelfOrNewMap(spec && spec.get(\"externalDocs\"))\n)\n\nexport const version = createSelector(\n\tinfo,\n\tinfo => info && info.get(\"version\")\n)\n\nexport const semver = createSelector(\n\tversion,\n\tversion => /v?([0-9]*)\\.([0-9]*)\\.([0-9]*)/i.exec(version).slice(1)\n)\n\nexport const paths = createSelector(\n\tspecJsonWithResolvedSubtrees,\n\tspec => spec.get(\"paths\")\n)\n\nexport const validOperationMethods = createSelector(() => [\"get\", \"put\", \"post\", \"delete\", \"options\", \"head\", \"patch\"])\n\nexport const operations = createSelector(\n paths,\n paths => {\n if(!paths || paths.size < 1)\n return List()\n\n let list = List()\n\n if(!paths || !paths.forEach) {\n return List()\n }\n\n paths.forEach((path, pathName) => {\n if(!path || !path.forEach) {\n return {}\n }\n path.forEach((operation, method) => {\n if(OPERATION_METHODS.indexOf(method) < 0) {\n return\n }\n list = list.push(fromJS({\n path: pathName,\n method,\n operation,\n id: `${method}-${pathName}`\n }))\n })\n })\n\n return list\n }\n)\n\nexport const consumes = createSelector(\n spec,\n spec => Set(spec.get(\"consumes\"))\n)\n\nexport const produces = createSelector(\n spec,\n spec => Set(spec.get(\"produces\"))\n)\n\nexport const security = createSelector(\n spec,\n spec => spec.get(\"security\", List())\n)\n\nexport const securityDefinitions = createSelector(\n spec,\n spec => spec.get(\"securityDefinitions\")\n)\n\n\nexport const findDefinition = ( state, name ) => {\n const resolvedRes = state.getIn([\"resolvedSubtrees\", \"definitions\", name], null)\n const unresolvedRes = state.getIn([\"json\", \"definitions\", name], null)\n return resolvedRes || unresolvedRes || null\n}\n\nexport const definitions = createSelector(\n spec,\n spec => {\n const res = spec.get(\"definitions\")\n return Map.isMap(res) ? res : Map()\n }\n)\n\nexport const basePath = createSelector(\n spec,\n spec => spec.get(\"basePath\")\n)\n\nexport const host = createSelector(\n spec,\n spec => spec.get(\"host\")\n)\n\nexport const schemes = createSelector(\n spec,\n spec => spec.get(\"schemes\", Map())\n)\n\nexport const operationsWithRootInherited = createSelector(\n operations,\n consumes,\n produces,\n (operations, consumes, produces) => {\n return operations.map( ops => ops.update(\"operation\", op => {\n if(op) {\n if(!Map.isMap(op)) { return }\n return op.withMutations( op => {\n if ( !op.get(\"consumes\") ) {\n op.update(\"consumes\", a => Set(a).merge(consumes))\n }\n if ( !op.get(\"produces\") ) {\n op.update(\"produces\", a => Set(a).merge(produces))\n }\n return op\n })\n } else {\n // return something with Immutable methods\n return Map()\n }\n\n }))\n }\n)\n\nexport const tags = createSelector(\n spec,\n json => {\n const tags = json.get(\"tags\", List())\n return List.isList(tags) ? tags.filter(tag => Map.isMap(tag)) : List()\n }\n)\n\nexport const tagDetails = (state, tag) => {\n let currentTags = tags(state) || List()\n return currentTags.filter(Map.isMap).find(t => t.get(\"name\") === tag, Map())\n}\n\nexport const operationsWithTags = createSelector(\n operationsWithRootInherited,\n tags,\n (operations, tags) => {\n return operations.reduce( (taggedMap, op) => {\n let tags = Set(op.getIn([\"operation\",\"tags\"]))\n if(tags.count() < 1)\n return taggedMap.update(DEFAULT_TAG, List(), ar => ar.push(op))\n return tags.reduce( (res, tag) => res.update(tag, List(), (ar) => ar.push(op)), taggedMap )\n }, tags.reduce( (taggedMap, tag) => {\n return taggedMap.set(tag.get(\"name\"), List())\n } , OrderedMap()))\n }\n)\n\nexport const taggedOperations = (state) => ({ getConfigs }) => {\n let { tagsSorter, operationsSorter } = getConfigs()\n return operationsWithTags(state)\n .sortBy(\n (val, key) => key, // get the name of the tag to be passed to the sorter\n (tagA, tagB) => {\n let sortFn = (typeof tagsSorter === \"function\" ? tagsSorter : sorters.tagsSorter[ tagsSorter ])\n return (!sortFn ? null : sortFn(tagA, tagB))\n }\n )\n .map((ops, tag) => {\n let sortFn = (typeof operationsSorter === \"function\" ? operationsSorter : sorters.operationsSorter[ operationsSorter ])\n let operations = (!sortFn ? ops : ops.sort(sortFn))\n\n return Map({ tagDetails: tagDetails(state, tag), operations: operations })\n })\n}\n\nexport const responses = createSelector(\n state,\n state => state.get( \"responses\", Map() )\n)\n\nexport const requests = createSelector(\n state,\n state => state.get( \"requests\", Map() )\n)\n\nexport const mutatedRequests = createSelector(\n state,\n state => state.get( \"mutatedRequests\", Map() )\n)\n\nexport const responseFor = (state, path, method) => {\n return responses(state).getIn([path, method], null)\n}\n\nexport const requestFor = (state, path, method) => {\n return requests(state).getIn([path, method], null)\n}\n\nexport const mutatedRequestFor = (state, path, method) => {\n return mutatedRequests(state).getIn([path, method], null)\n}\n\nexport const allowTryItOutFor = () => {\n // This is just a hook for now.\n return true\n}\n\nexport const parameterWithMetaByIdentity = (state, pathMethod, param) => {\n const opParams = specJsonWithResolvedSubtrees(state).getIn([\"paths\", ...pathMethod, \"parameters\"], OrderedMap())\n const metaParams = state.getIn([\"meta\", \"paths\", ...pathMethod, \"parameters\"], OrderedMap())\n\n const mergedParams = opParams.map((currentParam) => {\n const inNameKeyedMeta = metaParams.get(`${param.get(\"in\")}.${param.get(\"name\")}`)\n const hashKeyedMeta = metaParams.get(`${param.get(\"in\")}.${param.get(\"name\")}.hash-${param.hashCode()}`)\n return OrderedMap().merge(\n currentParam,\n inNameKeyedMeta,\n hashKeyedMeta\n )\n })\n return mergedParams.find(curr => curr.get(\"in\") === param.get(\"in\") && curr.get(\"name\") === param.get(\"name\"), OrderedMap())\n}\n\nexport const parameterInclusionSettingFor = (state, pathMethod, paramName, paramIn) => {\n const paramKey = `${paramIn}.${paramName}`\n return state.getIn([\"meta\", \"paths\", ...pathMethod, \"parameter_inclusions\", paramKey], false)\n}\n\n\nexport const parameterWithMeta = (state, pathMethod, paramName, paramIn) => {\n const opParams = specJsonWithResolvedSubtrees(state).getIn([\"paths\", ...pathMethod, \"parameters\"], OrderedMap())\n const currentParam = opParams.find(param => param.get(\"in\") === paramIn && param.get(\"name\") === paramName, OrderedMap())\n return parameterWithMetaByIdentity(state, pathMethod, currentParam)\n}\n\nexport const operationWithMeta = (state, path, method) => {\n const op = specJsonWithResolvedSubtrees(state).getIn([\"paths\", path, method], OrderedMap())\n const meta = state.getIn([\"meta\", \"paths\", path, method], OrderedMap())\n\n const mergedParams = op.get(\"parameters\", List()).map((param) => {\n return parameterWithMetaByIdentity(state, [path, method], param)\n })\n\n return OrderedMap()\n .merge(op, meta)\n .set(\"parameters\", mergedParams)\n}\n\n// Get the parameter value by parameter name\nexport function getParameter(state, pathMethod, name, inType) {\n pathMethod = pathMethod || []\n let params = state.getIn([\"meta\", \"paths\", ...pathMethod, \"parameters\"], fromJS([]))\n return params.find( (p) => {\n return Map.isMap(p) && p.get(\"name\") === name && p.get(\"in\") === inType\n }) || Map() // Always return a map\n}\n\nexport const hasHost = createSelector(\n spec,\n spec => {\n const host = spec.get(\"host\")\n return typeof host === \"string\" && host.length > 0 && host[0] !== \"/\"\n }\n)\n\n// Get the parameter values, that the user filled out\nexport function parameterValues(state, pathMethod, isXml) {\n pathMethod = pathMethod || []\n let paramValues = operationWithMeta(state, ...pathMethod).get(\"parameters\", List())\n return paramValues.reduce( (hash, p) => {\n let value = isXml && p.get(\"in\") === \"body\" ? p.get(\"value_xml\") : p.get(\"value\")\n return hash.set(paramToIdentifier(p, { allowHashes: false }), value)\n }, fromJS({}))\n}\n\n// True if any parameter includes `in: ?`\nexport function parametersIncludeIn(parameters, inValue=\"\") {\n if(List.isList(parameters)) {\n return parameters.some( p => Map.isMap(p) && p.get(\"in\") === inValue )\n }\n}\n\n// True if any parameter includes `type: ?`\nexport function parametersIncludeType(parameters, typeValue=\"\") {\n if(List.isList(parameters)) {\n return parameters.some( p => Map.isMap(p) && p.get(\"type\") === typeValue )\n }\n}\n\n// Get the consumes/produces value that the user selected\nexport function contentTypeValues(state, pathMethod) {\n pathMethod = pathMethod || []\n let op = specJsonWithResolvedSubtrees(state).getIn([\"paths\", ...pathMethod], fromJS({}))\n let meta = state.getIn([\"meta\", \"paths\", ...pathMethod], fromJS({}))\n let producesValue = currentProducesFor(state, pathMethod)\n\n const parameters = op.get(\"parameters\") || new List()\n\n const requestContentType = (\n meta.get(\"consumes_value\") ? meta.get(\"consumes_value\")\n : parametersIncludeType(parameters, \"file\") ? \"multipart/form-data\"\n : parametersIncludeType(parameters, \"formData\") ? \"application/x-www-form-urlencoded\"\n : undefined\n )\n\n return fromJS({\n requestContentType,\n responseContentType: producesValue\n })\n}\n\n// Get the currently selected produces value for an operation\nexport function currentProducesFor(state, pathMethod) {\n pathMethod = pathMethod || []\n\n const operation = specJsonWithResolvedSubtrees(state).getIn([ \"paths\", ...pathMethod], null)\n\n if(operation === null) {\n // return nothing if the operation does not exist\n return\n }\n\n const currentProducesValue = state.getIn([\"meta\", \"paths\", ...pathMethod, \"produces_value\"], null)\n const firstProducesArrayItem = operation.getIn([\"produces\", 0], null)\n\n return currentProducesValue || firstProducesArrayItem || \"application/json\"\n\n}\n\n// Get the produces options for an operation\nexport function producesOptionsFor(state, pathMethod) {\n pathMethod = pathMethod || []\n\n const spec = specJsonWithResolvedSubtrees(state)\n const operation = spec.getIn([ \"paths\", ...pathMethod], null)\n\n if(operation === null) {\n // return nothing if the operation does not exist\n return\n }\n\n const [path] = pathMethod\n\n const operationProduces = operation.get(\"produces\", null)\n const pathItemProduces = spec.getIn([\"paths\", path, \"produces\"], null)\n const globalProduces = spec.getIn([\"produces\"], null)\n\n return operationProduces || pathItemProduces || globalProduces\n}\n\n// Get the consumes options for an operation\nexport function consumesOptionsFor(state, pathMethod) {\n pathMethod = pathMethod || []\n\n const spec = specJsonWithResolvedSubtrees(state)\n const operation = spec.getIn([\"paths\", ...pathMethod], null)\n\n if (operation === null) {\n // return nothing if the operation does not exist\n return\n }\n\n const [path] = pathMethod\n\n const operationConsumes = operation.get(\"consumes\", null)\n const pathItemConsumes = spec.getIn([\"paths\", path, \"consumes\"], null)\n const globalConsumes = spec.getIn([\"consumes\"], null)\n\n return operationConsumes || pathItemConsumes || globalConsumes\n}\n\nexport const operationScheme = ( state, path, method ) => {\n let url = state.get(\"url\")\n let matchResult = url.match(/^([a-z][a-z0-9+\\-.]*):/)\n let urlScheme = Array.isArray(matchResult) ? matchResult[1] : null\n\n return state.getIn([\"scheme\", path, method]) || state.getIn([\"scheme\", \"_defaultScheme\"]) || urlScheme || \"\"\n}\n\nexport const canExecuteScheme = ( state, path, method ) => {\n return [\"http\", \"https\"].indexOf(operationScheme(state, path, method)) > -1\n}\n\nexport const validationErrors = (state, pathMethod) => {\n pathMethod = pathMethod || []\n let paramValues = state.getIn([\"meta\", \"paths\", ...pathMethod, \"parameters\"], fromJS([]))\n const result = []\n\n paramValues.forEach( (p) => {\n let errors = p.get(\"errors\")\n if ( errors && errors.count() ) {\n errors.forEach( e => result.push(e))\n }\n })\n\n return result\n}\n\nexport const validateBeforeExecute = (state, pathMethod) => {\n return validationErrors(state, pathMethod).length === 0\n}\n\nexport const getOAS3RequiredRequestBodyContentType = (state, pathMethod) => {\n let requiredObj = {\n requestBody: false,\n requestContentType: {}\n }\n let requestBody = state.getIn([\"resolvedSubtrees\", \"paths\", ...pathMethod, \"requestBody\"], fromJS([]))\n if (requestBody.size < 1) {\n return requiredObj\n }\n if (requestBody.getIn([\"required\"])) {\n requiredObj.requestBody = requestBody.getIn([\"required\"])\n }\n requestBody.getIn([\"content\"]).entrySeq().forEach((contentType) => { // e.g application/json\n const key = contentType[0]\n if (contentType[1].getIn([\"schema\", \"required\"])) {\n const val = contentType[1].getIn([\"schema\", \"required\"]).toJS()\n requiredObj.requestContentType[key] = val\n }\n })\n return requiredObj\n}\n\nexport const isMediaTypeSchemaPropertiesEqual = ( state, pathMethod, currentMediaType, targetMediaType) => {\n if((currentMediaType || targetMediaType) && currentMediaType === targetMediaType ) {\n return true\n }\n let requestBodyContent = state.getIn([\"resolvedSubtrees\", \"paths\", ...pathMethod, \"requestBody\", \"content\"], fromJS([]))\n if (requestBodyContent.size < 2 || !currentMediaType || !targetMediaType) {\n // nothing to compare\n return false\n }\n let currentMediaTypeSchemaProperties = requestBodyContent.getIn([currentMediaType, \"schema\", \"properties\"], fromJS([]))\n let targetMediaTypeSchemaProperties = requestBodyContent.getIn([targetMediaType, \"schema\", \"properties\"], fromJS([]))\n return !!currentMediaTypeSchemaProperties.equals(targetMediaTypeSchemaProperties)\n}\n\nfunction returnSelfOrNewMap(obj) {\n // returns obj if obj is an Immutable map, else returns a new Map\n return Map.isMap(obj) ? obj : new Map()\n}\n","import get from \"lodash/get\"\n\nexport const updateSpec = (ori, {specActions}) => (...args) => {\n ori(...args)\n specActions.parseToJson(...args)\n}\n\nexport const updateJsonSpec = (ori, {specActions}) => (...args) => {\n ori(...args)\n\n specActions.invalidateResolvedSubtreeCache()\n\n // Trigger resolution of any path-level $refs.\n const [json] = args\n const pathItems = get(json, [\"paths\"]) || {}\n const pathItemKeys = Object.keys(pathItems)\n\n pathItemKeys.forEach(k => {\n const val = get(pathItems, [k])\n\n if(val.$ref) {\n specActions.requestResolvedSubtree([\"paths\", k])\n }\n })\n\n // Trigger resolution of any securitySchemes-level $refs.\n specActions.requestResolvedSubtree([\"components\", \"securitySchemes\"])\n}\n\n// Log the request ( just for debugging, shouldn't affect prod )\nexport const executeRequest = (ori, { specActions }) => (req) => {\n specActions.logRequest(req)\n return ori(req)\n}\n\nexport const validateParams = (ori, { specSelectors }) => (req) => {\n return ori(req, specSelectors.isOAS3())\n}\n","export const loaded = (ori, system) => (...args) => {\n ori(...args)\n const value = system.getConfigs().withCredentials\n \n if(value !== undefined) {\n system.fn.fetch.withCredentials = typeof value === \"string\" ? (value === \"true\") : !!value\n }\n}\n","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_swagger_client_es_resolver_strategies_generic_08dd5200__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_swagger_client_es_resolver_strategies_openapi_2_ff6e79cf__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_swagger_client_es_resolver_strategies_openapi_3_0_2fa0ff7c__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_swagger_client_es_resolver_strategies_openapi_3_1_apidom_5e628d39__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"makeResolve\"]: () => __WEBPACK_EXTERNAL_MODULE_swagger_client_es_resolver_f879c638__.makeResolve });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"buildRequest\"]: () => __WEBPACK_EXTERNAL_MODULE_swagger_client_es_execute_d486d3d6__.buildRequest, [\"execute\"]: () => __WEBPACK_EXTERNAL_MODULE_swagger_client_es_execute_d486d3d6__.execute });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_swagger_client_es_http_69655560__[\"default\"], [\"makeHttp\"]: () => __WEBPACK_EXTERNAL_MODULE_swagger_client_es_http_69655560__.makeHttp, [\"serializeRes\"]: () => __WEBPACK_EXTERNAL_MODULE_swagger_client_es_http_69655560__.serializeRes });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"makeResolveSubtree\"]: () => __WEBPACK_EXTERNAL_MODULE_swagger_client_es_subtree_resolver_741cb9d9__.makeResolveSubtree });","import genericResolveStrategy from \"swagger-client/es/resolver/strategies/generic\"\nimport openApi2ResolveStrategy from \"swagger-client/es/resolver/strategies/openapi-2\"\nimport openApi30ResolveStrategy from \"swagger-client/es/resolver/strategies/openapi-3-0\"\nimport openApi31ApiDOMResolveStrategy from \"swagger-client/es/resolver/strategies/openapi-3-1-apidom\"\nimport { makeResolve } from \"swagger-client/es/resolver\"\nimport { execute, buildRequest } from \"swagger-client/es/execute\"\nimport Http, { makeHttp, serializeRes } from \"swagger-client/es/http\"\nimport { makeResolveSubtree } from \"swagger-client/es/subtree-resolver\"\nimport { opId } from \"swagger-client/es/helpers\"\nimport { loaded } from \"./configs-wrap-actions\"\n\nexport default function({ configs, getConfigs }) {\n return {\n fn: {\n fetch: makeHttp(Http, configs.preFetch, configs.postFetch),\n buildRequest,\n execute,\n resolve: makeResolve({\n strategies: [\n openApi31ApiDOMResolveStrategy,\n openApi30ResolveStrategy,\n openApi2ResolveStrategy,\n genericResolveStrategy,\n ],\n }),\n resolveSubtree: async (obj, path, options = {}) => {\n const freshConfigs = getConfigs()\n const defaultOptions = {\n modelPropertyMacro: freshConfigs.modelPropertyMacro,\n parameterMacro: freshConfigs.parameterMacro,\n requestInterceptor: freshConfigs.requestInterceptor,\n responseInterceptor: freshConfigs.responseInterceptor,\n strategies: [\n openApi31ApiDOMResolveStrategy,\n openApi30ResolveStrategy,\n openApi2ResolveStrategy,\n genericResolveStrategy,\n ],\n }\n\n return makeResolveSubtree(defaultOptions)(obj, path, options)\n },\n serializeRes,\n opId\n },\n statePlugins: {\n configs: {\n wrapActions: {\n loaded,\n }\n }\n },\n }\n}\n","import { shallowEqualKeys } from \"core/utils\"\n\nexport default function() {\n return {\n fn: { shallowEqualKeys }\n }\n}\n","export const getDisplayName = (WrappedComponent) => WrappedComponent.displayName || WrappedComponent.name || \"Component\"\n","import { memoize } from \"core/utils\"\n\nimport { getComponent, render, withMappedContainer } from \"./root-injects\"\nimport { getDisplayName } from \"./fn\"\nimport memoizeN from \"../../../helpers/memoizeN\"\n\nconst memoizeForGetComponent = (fn) => {\n const resolver = (...args) => JSON.stringify(args)\n return memoize(fn, resolver)\n}\n\nconst memoizeForWithMappedContainer = (fn) => {\n const resolver = (...args) => args\n return memoizeN(fn, resolver)\n}\n\nconst viewPlugin = ({getComponents, getStore, getSystem}) => {\n // getComponent should be passed into makeMappedContainer, _already_ memoized... otherwise we have a big performance hit ( think, really big )\n const memGetComponent = memoizeForGetComponent(getComponent(getSystem, getStore, getComponents))\n const memMakeMappedContainer = memoizeForWithMappedContainer(withMappedContainer(getSystem, getStore, memGetComponent))\n\n return {\n rootInjects: {\n getComponent: memGetComponent,\n makeMappedContainer: memMakeMappedContainer,\n render: render(getSystem, getStore, getComponent, getComponents),\n },\n fn: {\n getDisplayName,\n },\n }\n}\n\nexport default viewPlugin\n","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_react_dom_7dac9eee__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"Provider\"]: () => __WEBPACK_EXTERNAL_MODULE_react_redux_87be03b0__.Provider, [\"connect\"]: () => __WEBPACK_EXTERNAL_MODULE_react_redux_87be03b0__.connect });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_lodash_omit_d930e0f3__[\"default\"] });","import React, { Component } from \"react\"\nimport ReactDOM from \"react-dom\"\nimport { compose } from \"redux\"\nimport { connect, Provider } from \"react-redux\"\nimport omit from \"lodash/omit\"\nimport identity from \"lodash/identity\"\n\nconst withSystem = (getSystem) => (WrappedComponent) => {\n const { fn } = getSystem()\n\n class WithSystem extends Component {\n render() {\n return \n }\n }\n WithSystem.displayName = `WithSystem(${fn.getDisplayName(WrappedComponent)})`\n return WithSystem\n}\n\nconst withRoot = (getSystem, reduxStore) => (WrappedComponent) => {\n const { fn } = getSystem()\n\n class WithRoot extends Component {\n render() {\n return (\n \n \n \n )\n }\n }\n WithRoot.displayName = `WithRoot(${fn.getDisplayName(WrappedComponent)})`\n return WithRoot\n}\n\nconst withConnect = (getSystem, WrappedComponent, reduxStore) => {\n const mapStateToProps = (state, ownProps) => {\n const props = {...ownProps, ...getSystem()}\n const customMapStateToProps = WrappedComponent.prototype?.mapStateToProps || (state => ({state}))\n return customMapStateToProps(state, props)\n }\n\n return compose(\n reduxStore ? withRoot(getSystem, reduxStore) : identity,\n connect(mapStateToProps),\n withSystem(getSystem),\n )(WrappedComponent)\n}\n\nconst handleProps = (getSystem, mapping, props, oldProps) => {\n for (const prop in mapping) {\n const fn = mapping[prop]\n\n if (typeof fn === \"function\") {\n fn(props[prop], oldProps[prop], getSystem())\n }\n }\n}\n\nexport const withMappedContainer = (getSystem, getStore, memGetComponent) => (componentName, mapping) => {\n const { fn } = getSystem()\n const WrappedComponent = memGetComponent(componentName, \"root\")\n\n class WithMappedContainer extends Component {\n constructor(props, context) {\n super(props, context)\n handleProps(getSystem, mapping, props, {})\n }\n\n UNSAFE_componentWillReceiveProps(nextProps) {\n handleProps(getSystem, mapping, nextProps, this.props)\n }\n\n render() {\n const cleanProps = omit(this.props, mapping ? Object.keys(mapping) : [])\n return \n }\n }\n WithMappedContainer.displayName = `WithMappedContainer(${fn.getDisplayName(WrappedComponent)})`\n return WithMappedContainer\n}\n\nexport const render = (getSystem, getStore, getComponent, getComponents) => (domNode) => {\n const App = getComponent(getSystem, getStore, getComponents)(\"App\", \"root\")\n ReactDOM.render(, domNode)\n}\n\nexport const getComponent = (getSystem, getStore, getComponents) => (componentName, container, config = {}) => {\n\n if (typeof componentName !== \"string\")\n throw new TypeError(\"Need a string, to fetch a component. Was given a \" + typeof componentName)\n\n // getComponent has a config object as a third, optional parameter\n // using the config object requires the presence of the second parameter, container\n // e.g. getComponent(\"JsonSchema_string_whatever\", false, { failSilently: true })\n const component = getComponents(componentName)\n\n if (!component) {\n if (!config.failSilently) {\n getSystem().log.warn(\"Could not find component:\", componentName)\n }\n return null\n }\n\n if(!container) {\n return component\n }\n\n if(container === \"root\") {\n return withConnect(getSystem, component, getStore())\n }\n\n // container == truthy\n return withConnect(getSystem, component)\n}\n","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_react_syntax_highlighter_dist_esm_light_746e1958__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_react_syntax_highlighter_dist_esm_languages_hljs_javascript_e22911f7__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_react_syntax_highlighter_dist_esm_languages_hljs_json_b876afc5__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_react_syntax_highlighter_dist_esm_languages_hljs_xml_a81c807b__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_react_syntax_highlighter_dist_esm_languages_hljs_bash_1621c621__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_react_syntax_highlighter_dist_esm_languages_hljs_yaml_02838f34__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_react_syntax_highlighter_dist_esm_languages_hljs_http_4e924b23__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_react_syntax_highlighter_dist_esm_languages_hljs_powershell_d51eb4f6__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_react_syntax_highlighter_dist_esm_styles_hljs_agate_99a46aa2__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_react_syntax_highlighter_dist_esm_styles_hljs_arta_570691fc__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_react_syntax_highlighter_dist_esm_styles_hljs_monokai_2529bafb__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_react_syntax_highlighter_dist_esm_styles_hljs_nord_5bfa1099__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_react_syntax_highlighter_dist_esm_styles_hljs_obsidian_a278dd52__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_react_syntax_highlighter_dist_esm_styles_hljs_tomorrow_night_63765df9__[\"default\"] });","import SyntaxHighlighter from \"react-syntax-highlighter/dist/esm/light\"\nimport js from \"react-syntax-highlighter/dist/esm/languages/hljs/javascript\"\nimport json from \"react-syntax-highlighter/dist/esm/languages/hljs/json\"\nimport xml from \"react-syntax-highlighter/dist/esm/languages/hljs/xml\"\nimport bash from \"react-syntax-highlighter/dist/esm/languages/hljs/bash\"\nimport yaml from \"react-syntax-highlighter/dist/esm/languages/hljs/yaml\"\nimport http from \"react-syntax-highlighter/dist/esm/languages/hljs/http\"\nimport powershell from \"react-syntax-highlighter/dist/esm/languages/hljs/powershell\"\nimport javascript from \"react-syntax-highlighter/dist/esm/languages/hljs/javascript\"\n\nimport agate from \"react-syntax-highlighter/dist/esm/styles/hljs/agate\"\nimport arta from \"react-syntax-highlighter/dist/esm/styles/hljs/arta\"\nimport monokai from \"react-syntax-highlighter/dist/esm/styles/hljs/monokai\"\nimport nord from \"react-syntax-highlighter/dist/esm/styles/hljs/nord\"\nimport obsidian from \"react-syntax-highlighter/dist/esm/styles/hljs/obsidian\"\nimport tomorrowNight from \"react-syntax-highlighter/dist/esm/styles/hljs/tomorrow-night\"\n\nSyntaxHighlighter.registerLanguage(\"json\", json)\nSyntaxHighlighter.registerLanguage(\"js\", js)\nSyntaxHighlighter.registerLanguage(\"xml\", xml)\nSyntaxHighlighter.registerLanguage(\"yaml\", yaml)\nSyntaxHighlighter.registerLanguage(\"http\", http)\nSyntaxHighlighter.registerLanguage(\"bash\", bash)\nSyntaxHighlighter.registerLanguage(\"powershell\", powershell)\nSyntaxHighlighter.registerLanguage(\"javascript\", javascript)\n\nconst styles = {agate, arta, monokai, nord, obsidian, \"tomorrow-night\": tomorrowNight}\nexport const availableStyles = Object.keys(styles)\n\nexport const getStyle = name => {\n if (!availableStyles.includes(name)) {\n console.warn(`Request style '${name}' is not available, returning default instead`)\n return agate\n }\n return styles[name]\n}\n\nexport {SyntaxHighlighter, styles}\n","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_instance_starts_with_a4b73998__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"sanitizeUrl\"]: () => __WEBPACK_EXTERNAL_MODULE__braintree_sanitize_url_2340607f__.sanitizeUrl });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_lodash_camelCase_81fadc19__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_lodash_upperFirst_9993ecb4__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_lodash_find_e8ecc2cb__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_lodash_eq_b41b823a__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_css_escape_2d301448__[\"default\"] });","/*\n ATTENTION! This file (but not the functions within) is deprecated.\n\n You should probably add a new file to `./helpers/` instead of adding a new\n function here.\n\n One-function-per-file is a better pattern than what we have here.\n\n If you're refactoring something in here, feel free to break it out to a file\n in `./helpers` if you have the time.\n*/\nimport Im, { fromJS, Set } from \"immutable\"\nimport { sanitizeUrl as braintreeSanitizeUrl } from \"@braintree/sanitize-url\"\nimport camelCase from \"lodash/camelCase\"\nimport upperFirst from \"lodash/upperFirst\"\nimport _memoize from \"lodash/memoize\"\nimport find from \"lodash/find\"\nimport some from \"lodash/some\"\nimport eq from \"lodash/eq\"\nimport isFunction from \"lodash/isFunction\"\nimport win from \"./window\"\nimport cssEscape from \"css.escape\"\nimport getParameterSchema from \"../helpers/get-parameter-schema\"\nimport randomBytes from \"randombytes\"\nimport shaJs from \"sha.js\"\n\nconst DEFAULT_RESPONSE_KEY = \"default\"\n\nexport const isImmutable = (maybe) => Im.Iterable.isIterable(maybe)\n\nexport function objectify (thing) {\n if(!isObject(thing))\n return {}\n if(isImmutable(thing))\n return thing.toJS()\n return thing\n}\n\nexport function arrayify (thing) {\n if(!thing)\n return []\n\n if(thing.toArray)\n return thing.toArray()\n\n return normalizeArray(thing)\n}\n\nexport function fromJSOrdered(js) {\n if (isImmutable(js)) {\n return js // Can't do much here\n }\n if (js instanceof win.File) {\n return js\n }\n if (!isObject(js)) {\n return js\n }\n if (Array.isArray(js)) {\n return Im.Seq(js).map(fromJSOrdered).toList()\n }\n if (isFunction(js.entries)) {\n // handle multipart/form-data\n const objWithHashedKeys = createObjWithHashedKeys(js)\n return Im.OrderedMap(objWithHashedKeys).map(fromJSOrdered)\n }\n return Im.OrderedMap(js).map(fromJSOrdered)\n}\n\n/**\n * Convert a FormData object into plain object\n * Append a hashIdx and counter to the key name, if multiple exists\n * if single, key name = \n * if multiple, key name = \n * @example single entry for vegetable\n * fdObj.entries.vegtables: \"carrot\"\n * // returns newObj.vegetables : \"carrot\"\n * @example multiple entries for fruits[]\n * fdObj.entries.fruits[]: \"apple\"\n * // returns newObj.fruits[]_**[]1 : \"apple\"\n * fdObj.entries.fruits[]: \"banana\"\n * // returns newObj.fruits[]_**[]2 : \"banana\"\n * fdObj.entries.fruits[]: \"grape\"\n * // returns newObj.fruits[]_**[]3 : \"grape\"\n * @param {FormData} fdObj - a FormData object\n * @return {Object} - a plain object\n */\nexport function createObjWithHashedKeys (fdObj) {\n if (!isFunction(fdObj.entries)) {\n return fdObj // not a FormData object with iterable\n }\n const newObj = {}\n const hashIdx = \"_**[]\" // our internal identifier\n const trackKeys = {}\n for (let pair of fdObj.entries()) {\n if (!newObj[pair[0]] && !(trackKeys[pair[0]] && trackKeys[pair[0]].containsMultiple)) {\n newObj[pair[0]] = pair[1] // first key name: no hash required\n } else {\n if (!trackKeys[pair[0]]) {\n // initiate tracking key for multiple\n trackKeys[pair[0]] = {\n containsMultiple: true,\n length: 1\n }\n // \"reassign\" first pair to matching hashed format for multiple\n let hashedKeyFirst = `${pair[0]}${hashIdx}${trackKeys[pair[0]].length}`\n newObj[hashedKeyFirst] = newObj[pair[0]]\n // remove non-hashed key of multiple\n delete newObj[pair[0]] // first\n }\n trackKeys[pair[0]].length += 1\n let hashedKeyCurrent = `${pair[0]}${hashIdx}${trackKeys[pair[0]].length}`\n newObj[hashedKeyCurrent] = pair[1]\n }\n }\n return newObj\n}\n\nexport function bindToState(obj, state) {\n\tvar newObj = {}\n\tObject.keys(obj)\n .filter(key => typeof obj[key] === \"function\")\n .forEach(key => newObj[key] = obj[key].bind(null, state))\n\treturn newObj\n}\n\nexport function normalizeArray(arr) {\n if(Array.isArray(arr))\n return arr\n return [arr]\n}\n\nexport function isFn(fn) {\n return typeof fn === \"function\"\n}\n\nexport function isObject(obj) {\n return !!obj && typeof obj === \"object\"\n}\n\nexport function isFunc(thing) {\n return typeof(thing) === \"function\"\n}\n\nexport function isArray(thing) {\n return Array.isArray(thing)\n}\n\n// I've changed memoize libs more than once, so I'm using this a way to make that simpler\nexport const memoize = _memoize\n\nexport function objMap(obj, fn) {\n return Object.keys(obj).reduce((newObj, key) => {\n newObj[key] = fn(obj[key], key)\n return newObj\n }, {})\n}\n\nexport function objReduce(obj, fn) {\n return Object.keys(obj).reduce((newObj, key) => {\n let res = fn(obj[key], key)\n if(res && typeof res === \"object\")\n Object.assign(newObj, res)\n return newObj\n }, {})\n}\n\n// Redux middleware that exposes the system to async actions (like redux-thunk, but with out system instead of (dispatch, getState)\nexport function systemThunkMiddleware(getSystem) {\n return ({ dispatch, getState }) => { // eslint-disable-line no-unused-vars\n return next => action => {\n if (typeof action === \"function\") {\n return action(getSystem())\n }\n\n return next(action)\n }\n }\n}\n\nexport function defaultStatusCode ( responses ) {\n let codes = responses.keySeq()\n return codes.contains(DEFAULT_RESPONSE_KEY) ? DEFAULT_RESPONSE_KEY : codes.filter( key => (key+\"\")[0] === \"2\").sort().first()\n}\n\n\n/**\n * Returns an Immutable List, safely\n * @param {Immutable.Iterable} iterable the iterable to get the key from\n * @param {String|[String]} key either an array of keys, or a single key\n * @returns {Immutable.List} either iterable.get(keys) or an empty Immutable.List\n */\nexport function getList(iterable, keys) {\n if(!Im.Iterable.isIterable(iterable)) {\n return Im.List()\n }\n let val = iterable.getIn(Array.isArray(keys) ? keys : [keys])\n return Im.List.isList(val) ? val : Im.List()\n}\n\n/**\n * Take an immutable map, and convert to a list.\n * Where the keys are merged with the value objects\n * @param {Immutable.Map} map, the map to convert\n * @param {String} key the key to use, when merging the `key`\n * @returns {Immutable.List}\n */\nexport function mapToList(map, keyNames=\"key\", collectedKeys=Im.Map()) {\n if(!Im.Map.isMap(map) || !map.size) {\n return Im.List()\n }\n\n if(!Array.isArray(keyNames)) {\n keyNames = [ keyNames ]\n }\n\n if(keyNames.length < 1) {\n return map.merge(collectedKeys)\n }\n\n // I need to avoid `flatMap` from merging in the Maps, as well as the lists\n let list = Im.List()\n let keyName = keyNames[0]\n for(let entry of map.entries()) {\n let [key, val] = entry\n let nextList = mapToList(val, keyNames.slice(1), collectedKeys.set(keyName, key))\n if(Im.List.isList(nextList)) {\n list = list.concat(nextList)\n } else {\n list = list.push(nextList)\n }\n }\n\n return list\n}\n\nexport function extractFileNameFromContentDispositionHeader(value){\n let patterns = [\n /filename\\*=[^']+'\\w*'\"([^\"]+)\";?/i,\n /filename\\*=[^']+'\\w*'([^;]+);?/i,\n /filename=\"([^;]*);?\"/i,\n /filename=([^;]*);?/i\n ]\n\n let responseFilename\n patterns.some(regex => {\n responseFilename = regex.exec(value)\n return responseFilename !== null\n })\n\n if (responseFilename !== null && responseFilename.length > 1) {\n try {\n return decodeURIComponent(responseFilename[1])\n } catch(e) {\n console.error(e)\n }\n }\n\n return null\n}\n\n// PascalCase, aka UpperCamelCase\nexport function pascalCase(str) {\n return upperFirst(camelCase(str))\n}\n\n// Remove the ext of a filename, and pascalCase it\nexport function pascalCaseFilename(filename) {\n return pascalCase(filename.replace(/\\.[^./]*$/, \"\"))\n}\n\n// Check if ...\n// - new props\n// - If immutable, use .is()\n// - if in explicit objectList, then compare using _.eq\n// - else use ===\nexport const propChecker = (props, nextProps, objectList=[], ignoreList=[]) => {\n\n if(Object.keys(props).length !== Object.keys(nextProps).length) {\n return true\n }\n\n return (\n some(props, (a, name) => {\n if(ignoreList.includes(name)) {\n return false\n }\n let b = nextProps[name]\n\n if(Im.Iterable.isIterable(a)) {\n return !Im.is(a,b)\n }\n\n // Not going to compare objects\n if(typeof a === \"object\" && typeof b === \"object\") {\n return false\n }\n\n return a !== b\n })\n || objectList.some( objectPropName => !eq(props[objectPropName], nextProps[objectPropName])))\n}\n\nexport const validateMaximum = ( val, max ) => {\n if (val > max) {\n return `Value must be less than ${max}`\n }\n}\n\nexport const validateMinimum = ( val, min ) => {\n if (val < min) {\n return `Value must be greater than ${min}`\n }\n}\n\nexport const validateNumber = ( val ) => {\n if (!/^-?\\d+(\\.?\\d+)?$/.test(val)) {\n return \"Value must be a number\"\n }\n}\n\nexport const validateInteger = ( val ) => {\n if (!/^-?\\d+$/.test(val)) {\n return \"Value must be an integer\"\n }\n}\n\nexport const validateFile = ( val ) => {\n if ( val && !(val instanceof win.File) ) {\n return \"Value must be a file\"\n }\n}\n\nexport const validateBoolean = ( val ) => {\n if ( !(val === \"true\" || val === \"false\" || val === true || val === false) ) {\n return \"Value must be a boolean\"\n }\n}\n\nexport const validateString = ( val ) => {\n if ( val && typeof val !== \"string\" ) {\n return \"Value must be a string\"\n }\n}\n\nexport const validateDateTime = (val) => {\n if (isNaN(Date.parse(val))) {\n return \"Value must be a DateTime\"\n }\n}\n\nexport const validateGuid = (val) => {\n val = val.toString().toLowerCase()\n if (!/^[{(]?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}[)}]?$/.test(val)) {\n return \"Value must be a Guid\"\n }\n}\n\nexport const validateMaxLength = (val, max) => {\n if (val.length > max) {\n return `Value must be no longer than ${max} character${max !== 1 ? \"s\" : \"\"}`\n }\n}\n\nexport const validateUniqueItems = (val, uniqueItems) => {\n if (!val) {\n return\n }\n if (uniqueItems === \"true\" || uniqueItems === true) {\n const list = fromJS(val)\n const set = list.toSet()\n const hasDuplicates = val.length > set.size\n if(hasDuplicates) {\n let errorsPerIndex = Set()\n list.forEach((item, i) => {\n if(list.filter(v => isFunc(v.equals) ? v.equals(item) : v === item).size > 1) {\n errorsPerIndex = errorsPerIndex.add(i)\n }\n })\n if(errorsPerIndex.size !== 0) {\n return errorsPerIndex.map(i => ({index: i, error: \"No duplicates allowed.\"})).toArray()\n }\n }\n }\n}\n\nexport const validateMinItems = (val, min) => {\n if (!val && min >= 1 || val && val.length < min) {\n return `Array must contain at least ${min} item${min === 1 ? \"\" : \"s\"}`\n }\n}\n\nexport const validateMaxItems = (val, max) => {\n if (val && val.length > max) {\n return `Array must not contain more then ${max} item${max === 1 ? \"\" : \"s\"}`\n }\n}\n\nexport const validateMinLength = (val, min) => {\n if (val.length < min) {\n return `Value must be at least ${min} character${min !== 1 ? \"s\" : \"\"}`\n }\n}\n\nexport const validatePattern = (val, rxPattern) => {\n var patt = new RegExp(rxPattern)\n if (!patt.test(val)) {\n return \"Value must follow pattern \" + rxPattern\n }\n}\n\nfunction validateValueBySchema(value, schema, requiredByParam, bypassRequiredCheck, parameterContentMediaType) {\n if(!schema) return []\n let errors = []\n let nullable = schema.get(\"nullable\")\n let requiredBySchema = schema.get(\"required\")\n let maximum = schema.get(\"maximum\")\n let minimum = schema.get(\"minimum\")\n let type = schema.get(\"type\")\n let format = schema.get(\"format\")\n let maxLength = schema.get(\"maxLength\")\n let minLength = schema.get(\"minLength\")\n let uniqueItems = schema.get(\"uniqueItems\")\n let maxItems = schema.get(\"maxItems\")\n let minItems = schema.get(\"minItems\")\n let pattern = schema.get(\"pattern\")\n\n const schemaRequiresValue = requiredByParam || requiredBySchema === true\n const hasValue = value !== undefined && value !== null\n const isValidEmpty = !schemaRequiresValue && !hasValue\n\n const needsExplicitConstraintValidation = hasValue && type === \"array\"\n\n const requiresFurtherValidation =\n schemaRequiresValue\n || needsExplicitConstraintValidation\n || !isValidEmpty\n\n const isValidNullable = nullable && value === null\n\n // will not be included in the request or [schema / value] does not [allow / require] further analysis.\n const noFurtherValidationNeeded =\n isValidNullable\n || !type\n || !requiresFurtherValidation\n\n if(noFurtherValidationNeeded) {\n return []\n }\n\n // Further this point the parameter is considered worth to validate\n let stringCheck = type === \"string\" && value\n let arrayCheck = type === \"array\" && Array.isArray(value) && value.length\n let arrayListCheck = type === \"array\" && Im.List.isList(value) && value.count()\n let arrayStringCheck = type === \"array\" && typeof value === \"string\" && value\n let fileCheck = type === \"file\" && value instanceof win.File\n let booleanCheck = type === \"boolean\" && (value || value === false)\n let numberCheck = type === \"number\" && (value || value === 0)\n let integerCheck = type === \"integer\" && (value || value === 0)\n let objectCheck = type === \"object\" && typeof value === \"object\" && value !== null\n let objectStringCheck = type === \"object\" && typeof value === \"string\" && value\n\n const allChecks = [\n stringCheck, arrayCheck, arrayListCheck, arrayStringCheck, fileCheck,\n booleanCheck, numberCheck, integerCheck, objectCheck, objectStringCheck,\n ]\n\n const passedAnyCheck = allChecks.some(v => !!v)\n\n if (schemaRequiresValue && !passedAnyCheck && !bypassRequiredCheck) {\n errors.push(\"Required field is not provided\")\n return errors\n }\n if (\n type === \"object\" &&\n (parameterContentMediaType === null ||\n parameterContentMediaType === \"application/json\")\n ) {\n let objectVal = value\n if(typeof value === \"string\") {\n try {\n objectVal = JSON.parse(value)\n } catch (e) {\n errors.push(\"Parameter string value must be valid JSON\")\n return errors\n }\n }\n if(schema && schema.has(\"required\") && isFunc(requiredBySchema.isList) && requiredBySchema.isList()) {\n requiredBySchema.forEach(key => {\n if(objectVal[key] === undefined) {\n errors.push({ propKey: key, error: \"Required property not found\" })\n }\n })\n }\n if(schema && schema.has(\"properties\")) {\n schema.get(\"properties\").forEach((val, key) => {\n const errs = validateValueBySchema(objectVal[key], val, false, bypassRequiredCheck, parameterContentMediaType)\n errors.push(...errs\n .map((error) => ({ propKey: key, error })))\n })\n }\n }\n\n if (pattern) {\n let err = validatePattern(value, pattern)\n if (err) errors.push(err)\n }\n\n if (minItems) {\n if (type === \"array\") {\n let err = validateMinItems(value, minItems)\n if (err) errors.push(err)\n }\n }\n\n if (maxItems) {\n if (type === \"array\") {\n let err = validateMaxItems(value, maxItems)\n if (err) errors.push({ needRemove: true, error: err })\n }\n }\n\n if (uniqueItems) {\n if (type === \"array\") {\n let errorPerItem = validateUniqueItems(value, uniqueItems)\n if (errorPerItem) errors.push(...errorPerItem)\n }\n }\n\n if (maxLength || maxLength === 0) {\n let err = validateMaxLength(value, maxLength)\n if (err) errors.push(err)\n }\n\n if (minLength) {\n let err = validateMinLength(value, minLength)\n if (err) errors.push(err)\n }\n\n if (maximum || maximum === 0) {\n let err = validateMaximum(value, maximum)\n if (err) errors.push(err)\n }\n\n if (minimum || minimum === 0) {\n let err = validateMinimum(value, minimum)\n if (err) errors.push(err)\n }\n\n if (type === \"string\") {\n let err\n if (format === \"date-time\") {\n err = validateDateTime(value)\n } else if (format === \"uuid\") {\n err = validateGuid(value)\n } else {\n err = validateString(value)\n }\n if (!err) return errors\n errors.push(err)\n } else if (type === \"boolean\") {\n let err = validateBoolean(value)\n if (!err) return errors\n errors.push(err)\n } else if (type === \"number\") {\n let err = validateNumber(value)\n if (!err) return errors\n errors.push(err)\n } else if (type === \"integer\") {\n let err = validateInteger(value)\n if (!err) return errors\n errors.push(err)\n } else if (type === \"array\") {\n if (!(arrayCheck || arrayListCheck)) {\n return errors\n }\n if(value) {\n value.forEach((item, i) => {\n const errs = validateValueBySchema(item, schema.get(\"items\"), false, bypassRequiredCheck, parameterContentMediaType)\n errors.push(...errs\n .map((err) => ({ index: i, error: err })))\n })\n }\n } else if (type === \"file\") {\n let err = validateFile(value)\n if (!err) return errors\n errors.push(err)\n }\n\n return errors\n}\n\n// validation of parameters before execute\nexport const validateParam = (param, value, { isOAS3 = false, bypassRequiredCheck = false } = {}) => {\n\n let paramRequired = param.get(\"required\")\n\n let {\n schema: paramDetails,\n parameterContentMediaType\n } = getParameterSchema(param, { isOAS3 })\n\n return validateValueBySchema(value, paramDetails, paramRequired, bypassRequiredCheck, parameterContentMediaType)\n}\n\nexport const parseSearch = () => {\n let map = {}\n let search = win.location.search\n\n if(!search)\n return {}\n\n if ( search != \"\" ) {\n let params = search.substr(1).split(\"&\")\n\n for (let i in params) {\n if (!Object.prototype.hasOwnProperty.call(params, i)) {\n continue\n }\n i = params[i].split(\"=\")\n map[decodeURIComponent(i[0])] = (i[1] && decodeURIComponent(i[1])) || \"\"\n }\n }\n\n return map\n}\n\nexport const serializeSearch = (searchMap) => {\n return Object.keys(searchMap).map(k => {\n return encodeURIComponent(k) + \"=\" + encodeURIComponent(searchMap[k])\n }).join(\"&\")\n}\n\nexport const btoa = (str) => {\n let buffer\n\n if (str instanceof Buffer) {\n buffer = str\n } else {\n buffer = Buffer.from(str.toString(), \"utf-8\")\n }\n\n return buffer.toString(\"base64\")\n}\n\nexport const sorters = {\n operationsSorter: {\n alpha: (a, b) => a.get(\"path\").localeCompare(b.get(\"path\")),\n method: (a, b) => a.get(\"method\").localeCompare(b.get(\"method\"))\n },\n tagsSorter: {\n alpha: (a, b) => a.localeCompare(b)\n }\n}\n\nexport const buildFormData = (data) => {\n let formArr = []\n\n for (let name in data) {\n let val = data[name]\n if (val !== undefined && val !== \"\") {\n formArr.push([name, \"=\", encodeURIComponent(val).replace(/%20/g,\"+\")].join(\"\"))\n }\n }\n return formArr.join(\"&\")\n}\n\n// Is this really required as a helper? Perhaps. TODO: expose the system of presets.apis in docs, so we know what is supported\nexport const shallowEqualKeys = (a,b, keys) => {\n return !!find(keys, (key) => {\n return eq(a[key], b[key])\n })\n}\n\nexport function sanitizeUrl(url) {\n if(typeof url !== \"string\" || url === \"\") {\n return \"\"\n }\n\n return braintreeSanitizeUrl(url)\n}\n\nexport function requiresValidationURL(uri) {\n if (!uri || uri.indexOf(\"localhost\") >= 0 || uri.indexOf(\"127.0.0.1\") >= 0 || uri === \"none\") {\n return false\n }\n return true\n}\n\n\nexport function getAcceptControllingResponse(responses) {\n if(!Im.OrderedMap.isOrderedMap(responses)) {\n // wrong type!\n return null\n }\n\n if(!responses.size) {\n // responses is empty\n return null\n }\n\n const suitable2xxResponse = responses.find((res, k) => {\n return k.startsWith(\"2\") && Object.keys(res.get(\"content\") || {}).length > 0\n })\n\n // try to find a suitable `default` responses\n const defaultResponse = responses.get(\"default\") || Im.OrderedMap()\n const defaultResponseMediaTypes = (defaultResponse.get(\"content\") || Im.OrderedMap()).keySeq().toJS()\n const suitableDefaultResponse = defaultResponseMediaTypes.length ? defaultResponse : null\n\n return suitable2xxResponse || suitableDefaultResponse\n}\n\n// suitable for use in URL fragments\nexport const createDeepLinkPath = (str) => typeof str == \"string\" || str instanceof String ? str.trim().replace(/\\s/g, \"%20\") : \"\"\n// suitable for use in CSS classes and ids\nexport const escapeDeepLinkPath = (str) => cssEscape( createDeepLinkPath(str).replace(/%20/g, \"_\") )\n\nexport const getExtensions = (defObj) => defObj.filter((v, k) => /^x-/.test(k))\nexport const getCommonExtensions = (defObj) => defObj.filter((v, k) => /^pattern|maxLength|minLength|maximum|minimum/.test(k))\n\n// Deeply strips a specific key from an object.\n//\n// `predicate` can be used to discriminate the stripping further,\n// by preserving the key's place in the object based on its value.\nexport function deeplyStripKey(input, keyToStrip, predicate = () => true) {\n if(typeof input !== \"object\" || Array.isArray(input) || input === null || !keyToStrip) {\n return input\n }\n\n const obj = Object.assign({}, input)\n\n Object.keys(obj).forEach(k => {\n if(k === keyToStrip && predicate(obj[k], k)) {\n delete obj[k]\n return\n }\n obj[k] = deeplyStripKey(obj[k], keyToStrip, predicate)\n })\n\n return obj\n}\n\nexport function stringify(thing) {\n if (typeof thing === \"string\") {\n return thing\n }\n\n if (thing && thing.toJS) {\n thing = thing.toJS()\n }\n\n if (typeof thing === \"object\" && thing !== null) {\n try {\n return JSON.stringify(thing, null, 2)\n }\n catch (e) {\n return String(thing)\n }\n }\n\n if(thing === null || thing === undefined) {\n return \"\"\n }\n\n return thing.toString()\n}\n\nexport function numberToString(thing) {\n if(typeof thing === \"number\") {\n return thing.toString()\n }\n\n return thing\n}\n\nexport function paramToIdentifier(param, { returnAll = false, allowHashes = true } = {}) {\n if(!Im.Map.isMap(param)) {\n throw new Error(\"paramToIdentifier: received a non-Im.Map parameter as input\")\n }\n const paramName = param.get(\"name\")\n const paramIn = param.get(\"in\")\n\n let generatedIdentifiers = []\n\n // Generate identifiers in order of most to least specificity\n\n if (param && param.hashCode && paramIn && paramName && allowHashes) {\n generatedIdentifiers.push(`${paramIn}.${paramName}.hash-${param.hashCode()}`)\n }\n\n if(paramIn && paramName) {\n generatedIdentifiers.push(`${paramIn}.${paramName}`)\n }\n\n generatedIdentifiers.push(paramName)\n\n // Return the most preferred identifier, or all if requested\n\n return returnAll ? generatedIdentifiers : (generatedIdentifiers[0] || \"\")\n}\n\nexport function paramToValue(param, paramValues) {\n const allIdentifiers = paramToIdentifier(param, { returnAll: true })\n\n // Map identifiers to values in the provided value hash, filter undefined values,\n // and return the first value found\n const values = allIdentifiers\n .map(id => {\n return paramValues[id]\n })\n .filter(value => value !== undefined)\n\n return values[0]\n}\n\n// adapted from https://auth0.com/docs/flows/guides/auth-code-pkce/includes/create-code-verifier\nexport function generateCodeVerifier() {\n return b64toB64UrlEncoded(\n randomBytes(32).toString(\"base64\")\n )\n}\n\nexport function createCodeChallenge(codeVerifier) {\n return b64toB64UrlEncoded(\n shaJs(\"sha256\")\n .update(codeVerifier)\n .digest(\"base64\")\n )\n}\n\nfunction b64toB64UrlEncoded(str) {\n return str\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=/g, \"\")\n}\n\nexport const isEmptyValue = (value) => {\n if (!value) {\n return true\n }\n\n if (isImmutable(value) && value.isEmpty()) {\n return true\n }\n\n return false\n}\n","export function canJsonParse(str) {\n try {\n let testValueForJson = JSON.parse(str)\n return testValueForJson ? true : false\n } catch (e) {\n // exception: string is not valid json\n return null\n }\n}\n\nexport function getKnownSyntaxHighlighterLanguage(val) {\n // to start, only check for json. can expand as needed in future\n const isValidJson = canJsonParse(val)\n return isValidJson ? \"json\" : null\n}\n","export function isAbsoluteUrl(url) {\n return url.match(/^(?:[a-z]+:)?\\/\\//i) // Matches http://, HTTP://, https://, ftp://, //example.com,\n}\n\nexport function addProtocol(url) {\n if (!url.match(/^\\/\\//i)) return url // Checks if protocol is missing e.g. //example.com\n\n return `${window.location.protocol}${url}`\n}\n\nexport function buildBaseUrl(selectedServer, specUrl) {\n if (!selectedServer) return specUrl\n if (isAbsoluteUrl(selectedServer)) return addProtocol(selectedServer)\n\n return new URL(selectedServer, specUrl).href\n}\n\nexport function buildUrl(url, specUrl, { selectedServer=\"\" } = {}) {\n if (!url) return undefined\n if (isAbsoluteUrl(url)) return url\n\n const baseUrl = buildBaseUrl(selectedServer, specUrl)\n if (!isAbsoluteUrl(baseUrl)) {\n return new URL(url, window.location.href).href\n }\n return new URL(url, baseUrl).href\n}\n\n/**\n * Safe version of buildUrl function. `selectedServer` can contain server variables\n * which can fail the URL resolution.\n */\nexport function safeBuildUrl(url, specUrl, { selectedServer=\"\" } = {}) {\n try {\n return buildUrl(url, specUrl, { selectedServer })\n } catch {\n return undefined\n }\n}\n","function makeWindow() {\n var win = {\n location: {},\n history: {},\n open: () => {},\n close: () => {},\n File: function() {}\n }\n\n if(typeof window === \"undefined\") {\n return win\n }\n\n try {\n win = window\n var props = [\"File\", \"Blob\", \"FormData\"]\n for (var prop of props) {\n if (prop in window) {\n win[prop] = window[prop]\n }\n }\n } catch( e ) {\n console.error(e)\n }\n\n return win\n}\n\nexport default makeWindow()\n","/**\n * @prettier\n */\n\nimport Im from \"immutable\"\n\nconst swagger2SchemaKeys = Im.Set.of(\n \"type\",\n \"format\",\n \"items\",\n \"default\",\n \"maximum\",\n \"exclusiveMaximum\",\n \"minimum\",\n \"exclusiveMinimum\",\n \"maxLength\",\n \"minLength\",\n \"pattern\",\n \"maxItems\",\n \"minItems\",\n \"uniqueItems\",\n \"enum\",\n \"multipleOf\"\n)\n\n/**\n * @typedef {Object} ParameterSchemaDescriptor\n * @property {Immutable.Map} schema - the parameter schema\n * @property {string|null} parameterContentMediaType - the effective media type, for `content`-based OpenAPI 3.0 Parameters, or `null` otherwise\n */\n\n/**\n * Get the effective schema value for a parameter, or an empty Immutable.Map if\n * no suitable schema can be found.\n *\n * Supports OpenAPI 3.0 `Parameter.content` priority -- since a Parameter Object\n * cannot have both `schema` and `content`, this function ignores `schema` when\n * `content` is present.\n *\n * @param {Immutable.Map} parameter The parameter to identify a schema for\n * @param {object} config\n * @param {boolean} config.isOAS3 Whether the parameter is from an OpenAPI 2.0\n * or OpenAPI 3.0 definition\n * @return {ParameterSchemaDescriptor} Information about the parameter schema\n */\nexport default function getParameterSchema(parameter, { isOAS3 } = {}) {\n // Return empty Map if `parameter` isn't a Map\n if (!Im.Map.isMap(parameter)) {\n return {\n schema: Im.Map(),\n parameterContentMediaType: null,\n }\n }\n\n if (!isOAS3) {\n // Swagger 2.0\n if (parameter.get(\"in\") === \"body\") {\n return {\n schema: parameter.get(\"schema\", Im.Map()),\n parameterContentMediaType: null,\n }\n } else {\n return {\n schema: parameter.filter((v, k) => swagger2SchemaKeys.includes(k)),\n parameterContentMediaType: null,\n }\n }\n }\n\n // If we've reached here, the parameter is OpenAPI 3.0\n\n if (parameter.get(\"content\")) {\n const parameterContentMediaTypes = parameter\n .get(\"content\", Im.Map({}))\n .keySeq()\n\n const parameterContentMediaType = parameterContentMediaTypes.first()\n\n return {\n schema: parameter.getIn(\n [\"content\", parameterContentMediaType, \"schema\"],\n Im.Map()\n ),\n parameterContentMediaType,\n }\n }\n\n return {\n schema: parameter.get(\"schema\") ? parameter.get(\"schema\", Im.Map()): Im.Map(),\n parameterContentMediaType: null,\n }\n}\n","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_instance_find_index_99e05360__[\"default\"] });","import memoize from \"lodash/memoize\"\n\n/**\n * This function is extension on top of lodash.memoize.\n * It uses all the arguments of the `fn` as the cache key instead of just the first one.\n * If resolver is provided, it determines the cache key for\n * storing the result based on the arguments provided to the memoized function.\n */\n\nconst shallowArrayEquals = (a) => (b) => {\n return Array.isArray(a) && Array.isArray(b)\n && a.length === b.length\n && a.every((val, index) => val === b[index])\n}\n\nconst list = (...args) => args\n\nclass Cache extends Map {\n delete(key) {\n const keys = Array.from(this.keys())\n const foundKey = keys.find(shallowArrayEquals(key))\n return super.delete(foundKey)\n }\n\n get(key) {\n const keys = Array.from(this.keys())\n const foundKey = keys.find(shallowArrayEquals(key))\n return super.get(foundKey)\n }\n\n has(key) {\n const keys = Array.from(this.keys())\n return keys.findIndex(shallowArrayEquals(key)) !== -1\n }\n}\n\nconst memoizeN = (fn, resolver = list) => {\n const { Cache: OriginalCache } = memoize\n memoize.Cache = Cache\n\n const memoized = memoize(fn, resolver)\n\n memoize.Cache = OriginalCache\n\n return memoized\n}\n\nexport default memoizeN\n","/*!\n * The buffer module from node.js, for the browser.\n *\n * @author Feross Aboukhadijeh \n * @license MIT\n */\n/* eslint-disable no-proto */\n\n'use strict'\n\nconst base64 = require('base64-js')\nconst ieee754 = require('ieee754')\nconst customInspectSymbol =\n (typeof Symbol === 'function' && typeof Symbol['for'] === 'function') // eslint-disable-line dot-notation\n ? Symbol['for']('nodejs.util.inspect.custom') // eslint-disable-line dot-notation\n : null\n\nexports.Buffer = Buffer\nexports.SlowBuffer = SlowBuffer\nexports.INSPECT_MAX_BYTES = 50\n\nconst K_MAX_LENGTH = 0x7fffffff\nexports.kMaxLength = K_MAX_LENGTH\n\n/**\n * If `Buffer.TYPED_ARRAY_SUPPORT`:\n * === true Use Uint8Array implementation (fastest)\n * === false Print warning and recommend using `buffer` v4.x which has an Object\n * implementation (most compatible, even IE6)\n *\n * Browsers that support typed arrays are IE 10+, Firefox 4+, Chrome 7+, Safari 5.1+,\n * Opera 11.6+, iOS 4.2+.\n *\n * We report that the browser does not support typed arrays if the are not subclassable\n * using __proto__. Firefox 4-29 lacks support for adding new properties to `Uint8Array`\n * (See: https://bugzilla.mozilla.org/show_bug.cgi?id=695438). IE 10 lacks support\n * for __proto__ and has a buggy typed array implementation.\n */\nBuffer.TYPED_ARRAY_SUPPORT = typedArraySupport()\n\nif (!Buffer.TYPED_ARRAY_SUPPORT && typeof console !== 'undefined' &&\n typeof console.error === 'function') {\n console.error(\n 'This browser lacks typed array (Uint8Array) support which is required by ' +\n '`buffer` v5.x. Use `buffer` v4.x if you require old browser support.'\n )\n}\n\nfunction typedArraySupport () {\n // Can typed array instances can be augmented?\n try {\n const arr = new Uint8Array(1)\n const proto = { foo: function () { return 42 } }\n Object.setPrototypeOf(proto, Uint8Array.prototype)\n Object.setPrototypeOf(arr, proto)\n return arr.foo() === 42\n } catch (e) {\n return false\n }\n}\n\nObject.defineProperty(Buffer.prototype, 'parent', {\n enumerable: true,\n get: function () {\n if (!Buffer.isBuffer(this)) return undefined\n return this.buffer\n }\n})\n\nObject.defineProperty(Buffer.prototype, 'offset', {\n enumerable: true,\n get: function () {\n if (!Buffer.isBuffer(this)) return undefined\n return this.byteOffset\n }\n})\n\nfunction createBuffer (length) {\n if (length > K_MAX_LENGTH) {\n throw new RangeError('The value \"' + length + '\" is invalid for option \"size\"')\n }\n // Return an augmented `Uint8Array` instance\n const buf = new Uint8Array(length)\n Object.setPrototypeOf(buf, Buffer.prototype)\n return buf\n}\n\n/**\n * The Buffer constructor returns instances of `Uint8Array` that have their\n * prototype changed to `Buffer.prototype`. Furthermore, `Buffer` is a subclass of\n * `Uint8Array`, so the returned instances will have all the node `Buffer` methods\n * and the `Uint8Array` methods. Square bracket notation works as expected -- it\n * returns a single octet.\n *\n * The `Uint8Array` prototype remains unmodified.\n */\n\nfunction Buffer (arg, encodingOrOffset, length) {\n // Common case.\n if (typeof arg === 'number') {\n if (typeof encodingOrOffset === 'string') {\n throw new TypeError(\n 'The \"string\" argument must be of type string. Received type number'\n )\n }\n return allocUnsafe(arg)\n }\n return from(arg, encodingOrOffset, length)\n}\n\nBuffer.poolSize = 8192 // not used by this implementation\n\nfunction from (value, encodingOrOffset, length) {\n if (typeof value === 'string') {\n return fromString(value, encodingOrOffset)\n }\n\n if (ArrayBuffer.isView(value)) {\n return fromArrayView(value)\n }\n\n if (value == null) {\n throw new TypeError(\n 'The first argument must be one of type string, Buffer, ArrayBuffer, Array, ' +\n 'or Array-like Object. Received type ' + (typeof value)\n )\n }\n\n if (isInstance(value, ArrayBuffer) ||\n (value && isInstance(value.buffer, ArrayBuffer))) {\n return fromArrayBuffer(value, encodingOrOffset, length)\n }\n\n if (typeof SharedArrayBuffer !== 'undefined' &&\n (isInstance(value, SharedArrayBuffer) ||\n (value && isInstance(value.buffer, SharedArrayBuffer)))) {\n return fromArrayBuffer(value, encodingOrOffset, length)\n }\n\n if (typeof value === 'number') {\n throw new TypeError(\n 'The \"value\" argument must not be of type number. Received type number'\n )\n }\n\n const valueOf = value.valueOf && value.valueOf()\n if (valueOf != null && valueOf !== value) {\n return Buffer.from(valueOf, encodingOrOffset, length)\n }\n\n const b = fromObject(value)\n if (b) return b\n\n if (typeof Symbol !== 'undefined' && Symbol.toPrimitive != null &&\n typeof value[Symbol.toPrimitive] === 'function') {\n return Buffer.from(value[Symbol.toPrimitive]('string'), encodingOrOffset, length)\n }\n\n throw new TypeError(\n 'The first argument must be one of type string, Buffer, ArrayBuffer, Array, ' +\n 'or Array-like Object. Received type ' + (typeof value)\n )\n}\n\n/**\n * Functionally equivalent to Buffer(arg, encoding) but throws a TypeError\n * if value is a number.\n * Buffer.from(str[, encoding])\n * Buffer.from(array)\n * Buffer.from(buffer)\n * Buffer.from(arrayBuffer[, byteOffset[, length]])\n **/\nBuffer.from = function (value, encodingOrOffset, length) {\n return from(value, encodingOrOffset, length)\n}\n\n// Note: Change prototype *after* Buffer.from is defined to workaround Chrome bug:\n// https://github.com/feross/buffer/pull/148\nObject.setPrototypeOf(Buffer.prototype, Uint8Array.prototype)\nObject.setPrototypeOf(Buffer, Uint8Array)\n\nfunction assertSize (size) {\n if (typeof size !== 'number') {\n throw new TypeError('\"size\" argument must be of type number')\n } else if (size < 0) {\n throw new RangeError('The value \"' + size + '\" is invalid for option \"size\"')\n }\n}\n\nfunction alloc (size, fill, encoding) {\n assertSize(size)\n if (size <= 0) {\n return createBuffer(size)\n }\n if (fill !== undefined) {\n // Only pay attention to encoding if it's a string. This\n // prevents accidentally sending in a number that would\n // be interpreted as a start offset.\n return typeof encoding === 'string'\n ? createBuffer(size).fill(fill, encoding)\n : createBuffer(size).fill(fill)\n }\n return createBuffer(size)\n}\n\n/**\n * Creates a new filled Buffer instance.\n * alloc(size[, fill[, encoding]])\n **/\nBuffer.alloc = function (size, fill, encoding) {\n return alloc(size, fill, encoding)\n}\n\nfunction allocUnsafe (size) {\n assertSize(size)\n return createBuffer(size < 0 ? 0 : checked(size) | 0)\n}\n\n/**\n * Equivalent to Buffer(num), by default creates a non-zero-filled Buffer instance.\n * */\nBuffer.allocUnsafe = function (size) {\n return allocUnsafe(size)\n}\n/**\n * Equivalent to SlowBuffer(num), by default creates a non-zero-filled Buffer instance.\n */\nBuffer.allocUnsafeSlow = function (size) {\n return allocUnsafe(size)\n}\n\nfunction fromString (string, encoding) {\n if (typeof encoding !== 'string' || encoding === '') {\n encoding = 'utf8'\n }\n\n if (!Buffer.isEncoding(encoding)) {\n throw new TypeError('Unknown encoding: ' + encoding)\n }\n\n const length = byteLength(string, encoding) | 0\n let buf = createBuffer(length)\n\n const actual = buf.write(string, encoding)\n\n if (actual !== length) {\n // Writing a hex string, for example, that contains invalid characters will\n // cause everything after the first invalid character to be ignored. (e.g.\n // 'abxxcd' will be treated as 'ab')\n buf = buf.slice(0, actual)\n }\n\n return buf\n}\n\nfunction fromArrayLike (array) {\n const length = array.length < 0 ? 0 : checked(array.length) | 0\n const buf = createBuffer(length)\n for (let i = 0; i < length; i += 1) {\n buf[i] = array[i] & 255\n }\n return buf\n}\n\nfunction fromArrayView (arrayView) {\n if (isInstance(arrayView, Uint8Array)) {\n const copy = new Uint8Array(arrayView)\n return fromArrayBuffer(copy.buffer, copy.byteOffset, copy.byteLength)\n }\n return fromArrayLike(arrayView)\n}\n\nfunction fromArrayBuffer (array, byteOffset, length) {\n if (byteOffset < 0 || array.byteLength < byteOffset) {\n throw new RangeError('\"offset\" is outside of buffer bounds')\n }\n\n if (array.byteLength < byteOffset + (length || 0)) {\n throw new RangeError('\"length\" is outside of buffer bounds')\n }\n\n let buf\n if (byteOffset === undefined && length === undefined) {\n buf = new Uint8Array(array)\n } else if (length === undefined) {\n buf = new Uint8Array(array, byteOffset)\n } else {\n buf = new Uint8Array(array, byteOffset, length)\n }\n\n // Return an augmented `Uint8Array` instance\n Object.setPrototypeOf(buf, Buffer.prototype)\n\n return buf\n}\n\nfunction fromObject (obj) {\n if (Buffer.isBuffer(obj)) {\n const len = checked(obj.length) | 0\n const buf = createBuffer(len)\n\n if (buf.length === 0) {\n return buf\n }\n\n obj.copy(buf, 0, 0, len)\n return buf\n }\n\n if (obj.length !== undefined) {\n if (typeof obj.length !== 'number' || numberIsNaN(obj.length)) {\n return createBuffer(0)\n }\n return fromArrayLike(obj)\n }\n\n if (obj.type === 'Buffer' && Array.isArray(obj.data)) {\n return fromArrayLike(obj.data)\n }\n}\n\nfunction checked (length) {\n // Note: cannot use `length < K_MAX_LENGTH` here because that fails when\n // length is NaN (which is otherwise coerced to zero.)\n if (length >= K_MAX_LENGTH) {\n throw new RangeError('Attempt to allocate Buffer larger than maximum ' +\n 'size: 0x' + K_MAX_LENGTH.toString(16) + ' bytes')\n }\n return length | 0\n}\n\nfunction SlowBuffer (length) {\n if (+length != length) { // eslint-disable-line eqeqeq\n length = 0\n }\n return Buffer.alloc(+length)\n}\n\nBuffer.isBuffer = function isBuffer (b) {\n return b != null && b._isBuffer === true &&\n b !== Buffer.prototype // so Buffer.isBuffer(Buffer.prototype) will be false\n}\n\nBuffer.compare = function compare (a, b) {\n if (isInstance(a, Uint8Array)) a = Buffer.from(a, a.offset, a.byteLength)\n if (isInstance(b, Uint8Array)) b = Buffer.from(b, b.offset, b.byteLength)\n if (!Buffer.isBuffer(a) || !Buffer.isBuffer(b)) {\n throw new TypeError(\n 'The \"buf1\", \"buf2\" arguments must be one of type Buffer or Uint8Array'\n )\n }\n\n if (a === b) return 0\n\n let x = a.length\n let y = b.length\n\n for (let i = 0, len = Math.min(x, y); i < len; ++i) {\n if (a[i] !== b[i]) {\n x = a[i]\n y = b[i]\n break\n }\n }\n\n if (x < y) return -1\n if (y < x) return 1\n return 0\n}\n\nBuffer.isEncoding = function isEncoding (encoding) {\n switch (String(encoding).toLowerCase()) {\n case 'hex':\n case 'utf8':\n case 'utf-8':\n case 'ascii':\n case 'latin1':\n case 'binary':\n case 'base64':\n case 'ucs2':\n case 'ucs-2':\n case 'utf16le':\n case 'utf-16le':\n return true\n default:\n return false\n }\n}\n\nBuffer.concat = function concat (list, length) {\n if (!Array.isArray(list)) {\n throw new TypeError('\"list\" argument must be an Array of Buffers')\n }\n\n if (list.length === 0) {\n return Buffer.alloc(0)\n }\n\n let i\n if (length === undefined) {\n length = 0\n for (i = 0; i < list.length; ++i) {\n length += list[i].length\n }\n }\n\n const buffer = Buffer.allocUnsafe(length)\n let pos = 0\n for (i = 0; i < list.length; ++i) {\n let buf = list[i]\n if (isInstance(buf, Uint8Array)) {\n if (pos + buf.length > buffer.length) {\n if (!Buffer.isBuffer(buf)) buf = Buffer.from(buf)\n buf.copy(buffer, pos)\n } else {\n Uint8Array.prototype.set.call(\n buffer,\n buf,\n pos\n )\n }\n } else if (!Buffer.isBuffer(buf)) {\n throw new TypeError('\"list\" argument must be an Array of Buffers')\n } else {\n buf.copy(buffer, pos)\n }\n pos += buf.length\n }\n return buffer\n}\n\nfunction byteLength (string, encoding) {\n if (Buffer.isBuffer(string)) {\n return string.length\n }\n if (ArrayBuffer.isView(string) || isInstance(string, ArrayBuffer)) {\n return string.byteLength\n }\n if (typeof string !== 'string') {\n throw new TypeError(\n 'The \"string\" argument must be one of type string, Buffer, or ArrayBuffer. ' +\n 'Received type ' + typeof string\n )\n }\n\n const len = string.length\n const mustMatch = (arguments.length > 2 && arguments[2] === true)\n if (!mustMatch && len === 0) return 0\n\n // Use a for loop to avoid recursion\n let loweredCase = false\n for (;;) {\n switch (encoding) {\n case 'ascii':\n case 'latin1':\n case 'binary':\n return len\n case 'utf8':\n case 'utf-8':\n return utf8ToBytes(string).length\n case 'ucs2':\n case 'ucs-2':\n case 'utf16le':\n case 'utf-16le':\n return len * 2\n case 'hex':\n return len >>> 1\n case 'base64':\n return base64ToBytes(string).length\n default:\n if (loweredCase) {\n return mustMatch ? -1 : utf8ToBytes(string).length // assume utf8\n }\n encoding = ('' + encoding).toLowerCase()\n loweredCase = true\n }\n }\n}\nBuffer.byteLength = byteLength\n\nfunction slowToString (encoding, start, end) {\n let loweredCase = false\n\n // No need to verify that \"this.length <= MAX_UINT32\" since it's a read-only\n // property of a typed array.\n\n // This behaves neither like String nor Uint8Array in that we set start/end\n // to their upper/lower bounds if the value passed is out of range.\n // undefined is handled specially as per ECMA-262 6th Edition,\n // Section 13.3.3.7 Runtime Semantics: KeyedBindingInitialization.\n if (start === undefined || start < 0) {\n start = 0\n }\n // Return early if start > this.length. Done here to prevent potential uint32\n // coercion fail below.\n if (start > this.length) {\n return ''\n }\n\n if (end === undefined || end > this.length) {\n end = this.length\n }\n\n if (end <= 0) {\n return ''\n }\n\n // Force coercion to uint32. This will also coerce falsey/NaN values to 0.\n end >>>= 0\n start >>>= 0\n\n if (end <= start) {\n return ''\n }\n\n if (!encoding) encoding = 'utf8'\n\n while (true) {\n switch (encoding) {\n case 'hex':\n return hexSlice(this, start, end)\n\n case 'utf8':\n case 'utf-8':\n return utf8Slice(this, start, end)\n\n case 'ascii':\n return asciiSlice(this, start, end)\n\n case 'latin1':\n case 'binary':\n return latin1Slice(this, start, end)\n\n case 'base64':\n return base64Slice(this, start, end)\n\n case 'ucs2':\n case 'ucs-2':\n case 'utf16le':\n case 'utf-16le':\n return utf16leSlice(this, start, end)\n\n default:\n if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding)\n encoding = (encoding + '').toLowerCase()\n loweredCase = true\n }\n }\n}\n\n// This property is used by `Buffer.isBuffer` (and the `is-buffer` npm package)\n// to detect a Buffer instance. It's not possible to use `instanceof Buffer`\n// reliably in a browserify context because there could be multiple different\n// copies of the 'buffer' package in use. This method works even for Buffer\n// instances that were created from another copy of the `buffer` package.\n// See: https://github.com/feross/buffer/issues/154\nBuffer.prototype._isBuffer = true\n\nfunction swap (b, n, m) {\n const i = b[n]\n b[n] = b[m]\n b[m] = i\n}\n\nBuffer.prototype.swap16 = function swap16 () {\n const len = this.length\n if (len % 2 !== 0) {\n throw new RangeError('Buffer size must be a multiple of 16-bits')\n }\n for (let i = 0; i < len; i += 2) {\n swap(this, i, i + 1)\n }\n return this\n}\n\nBuffer.prototype.swap32 = function swap32 () {\n const len = this.length\n if (len % 4 !== 0) {\n throw new RangeError('Buffer size must be a multiple of 32-bits')\n }\n for (let i = 0; i < len; i += 4) {\n swap(this, i, i + 3)\n swap(this, i + 1, i + 2)\n }\n return this\n}\n\nBuffer.prototype.swap64 = function swap64 () {\n const len = this.length\n if (len % 8 !== 0) {\n throw new RangeError('Buffer size must be a multiple of 64-bits')\n }\n for (let i = 0; i < len; i += 8) {\n swap(this, i, i + 7)\n swap(this, i + 1, i + 6)\n swap(this, i + 2, i + 5)\n swap(this, i + 3, i + 4)\n }\n return this\n}\n\nBuffer.prototype.toString = function toString () {\n const length = this.length\n if (length === 0) return ''\n if (arguments.length === 0) return utf8Slice(this, 0, length)\n return slowToString.apply(this, arguments)\n}\n\nBuffer.prototype.toLocaleString = Buffer.prototype.toString\n\nBuffer.prototype.equals = function equals (b) {\n if (!Buffer.isBuffer(b)) throw new TypeError('Argument must be a Buffer')\n if (this === b) return true\n return Buffer.compare(this, b) === 0\n}\n\nBuffer.prototype.inspect = function inspect () {\n let str = ''\n const max = exports.INSPECT_MAX_BYTES\n str = this.toString('hex', 0, max).replace(/(.{2})/g, '$1 ').trim()\n if (this.length > max) str += ' ... '\n return ''\n}\nif (customInspectSymbol) {\n Buffer.prototype[customInspectSymbol] = Buffer.prototype.inspect\n}\n\nBuffer.prototype.compare = function compare (target, start, end, thisStart, thisEnd) {\n if (isInstance(target, Uint8Array)) {\n target = Buffer.from(target, target.offset, target.byteLength)\n }\n if (!Buffer.isBuffer(target)) {\n throw new TypeError(\n 'The \"target\" argument must be one of type Buffer or Uint8Array. ' +\n 'Received type ' + (typeof target)\n )\n }\n\n if (start === undefined) {\n start = 0\n }\n if (end === undefined) {\n end = target ? target.length : 0\n }\n if (thisStart === undefined) {\n thisStart = 0\n }\n if (thisEnd === undefined) {\n thisEnd = this.length\n }\n\n if (start < 0 || end > target.length || thisStart < 0 || thisEnd > this.length) {\n throw new RangeError('out of range index')\n }\n\n if (thisStart >= thisEnd && start >= end) {\n return 0\n }\n if (thisStart >= thisEnd) {\n return -1\n }\n if (start >= end) {\n return 1\n }\n\n start >>>= 0\n end >>>= 0\n thisStart >>>= 0\n thisEnd >>>= 0\n\n if (this === target) return 0\n\n let x = thisEnd - thisStart\n let y = end - start\n const len = Math.min(x, y)\n\n const thisCopy = this.slice(thisStart, thisEnd)\n const targetCopy = target.slice(start, end)\n\n for (let i = 0; i < len; ++i) {\n if (thisCopy[i] !== targetCopy[i]) {\n x = thisCopy[i]\n y = targetCopy[i]\n break\n }\n }\n\n if (x < y) return -1\n if (y < x) return 1\n return 0\n}\n\n// Finds either the first index of `val` in `buffer` at offset >= `byteOffset`,\n// OR the last index of `val` in `buffer` at offset <= `byteOffset`.\n//\n// Arguments:\n// - buffer - a Buffer to search\n// - val - a string, Buffer, or number\n// - byteOffset - an index into `buffer`; will be clamped to an int32\n// - encoding - an optional encoding, relevant is val is a string\n// - dir - true for indexOf, false for lastIndexOf\nfunction bidirectionalIndexOf (buffer, val, byteOffset, encoding, dir) {\n // Empty buffer means no match\n if (buffer.length === 0) return -1\n\n // Normalize byteOffset\n if (typeof byteOffset === 'string') {\n encoding = byteOffset\n byteOffset = 0\n } else if (byteOffset > 0x7fffffff) {\n byteOffset = 0x7fffffff\n } else if (byteOffset < -0x80000000) {\n byteOffset = -0x80000000\n }\n byteOffset = +byteOffset // Coerce to Number.\n if (numberIsNaN(byteOffset)) {\n // byteOffset: it it's undefined, null, NaN, \"foo\", etc, search whole buffer\n byteOffset = dir ? 0 : (buffer.length - 1)\n }\n\n // Normalize byteOffset: negative offsets start from the end of the buffer\n if (byteOffset < 0) byteOffset = buffer.length + byteOffset\n if (byteOffset >= buffer.length) {\n if (dir) return -1\n else byteOffset = buffer.length - 1\n } else if (byteOffset < 0) {\n if (dir) byteOffset = 0\n else return -1\n }\n\n // Normalize val\n if (typeof val === 'string') {\n val = Buffer.from(val, encoding)\n }\n\n // Finally, search either indexOf (if dir is true) or lastIndexOf\n if (Buffer.isBuffer(val)) {\n // Special case: looking for empty string/buffer always fails\n if (val.length === 0) {\n return -1\n }\n return arrayIndexOf(buffer, val, byteOffset, encoding, dir)\n } else if (typeof val === 'number') {\n val = val & 0xFF // Search for a byte value [0-255]\n if (typeof Uint8Array.prototype.indexOf === 'function') {\n if (dir) {\n return Uint8Array.prototype.indexOf.call(buffer, val, byteOffset)\n } else {\n return Uint8Array.prototype.lastIndexOf.call(buffer, val, byteOffset)\n }\n }\n return arrayIndexOf(buffer, [val], byteOffset, encoding, dir)\n }\n\n throw new TypeError('val must be string, number or Buffer')\n}\n\nfunction arrayIndexOf (arr, val, byteOffset, encoding, dir) {\n let indexSize = 1\n let arrLength = arr.length\n let valLength = val.length\n\n if (encoding !== undefined) {\n encoding = String(encoding).toLowerCase()\n if (encoding === 'ucs2' || encoding === 'ucs-2' ||\n encoding === 'utf16le' || encoding === 'utf-16le') {\n if (arr.length < 2 || val.length < 2) {\n return -1\n }\n indexSize = 2\n arrLength /= 2\n valLength /= 2\n byteOffset /= 2\n }\n }\n\n function read (buf, i) {\n if (indexSize === 1) {\n return buf[i]\n } else {\n return buf.readUInt16BE(i * indexSize)\n }\n }\n\n let i\n if (dir) {\n let foundIndex = -1\n for (i = byteOffset; i < arrLength; i++) {\n if (read(arr, i) === read(val, foundIndex === -1 ? 0 : i - foundIndex)) {\n if (foundIndex === -1) foundIndex = i\n if (i - foundIndex + 1 === valLength) return foundIndex * indexSize\n } else {\n if (foundIndex !== -1) i -= i - foundIndex\n foundIndex = -1\n }\n }\n } else {\n if (byteOffset + valLength > arrLength) byteOffset = arrLength - valLength\n for (i = byteOffset; i >= 0; i--) {\n let found = true\n for (let j = 0; j < valLength; j++) {\n if (read(arr, i + j) !== read(val, j)) {\n found = false\n break\n }\n }\n if (found) return i\n }\n }\n\n return -1\n}\n\nBuffer.prototype.includes = function includes (val, byteOffset, encoding) {\n return this.indexOf(val, byteOffset, encoding) !== -1\n}\n\nBuffer.prototype.indexOf = function indexOf (val, byteOffset, encoding) {\n return bidirectionalIndexOf(this, val, byteOffset, encoding, true)\n}\n\nBuffer.prototype.lastIndexOf = function lastIndexOf (val, byteOffset, encoding) {\n return bidirectionalIndexOf(this, val, byteOffset, encoding, false)\n}\n\nfunction hexWrite (buf, string, offset, length) {\n offset = Number(offset) || 0\n const remaining = buf.length - offset\n if (!length) {\n length = remaining\n } else {\n length = Number(length)\n if (length > remaining) {\n length = remaining\n }\n }\n\n const strLen = string.length\n\n if (length > strLen / 2) {\n length = strLen / 2\n }\n let i\n for (i = 0; i < length; ++i) {\n const parsed = parseInt(string.substr(i * 2, 2), 16)\n if (numberIsNaN(parsed)) return i\n buf[offset + i] = parsed\n }\n return i\n}\n\nfunction utf8Write (buf, string, offset, length) {\n return blitBuffer(utf8ToBytes(string, buf.length - offset), buf, offset, length)\n}\n\nfunction asciiWrite (buf, string, offset, length) {\n return blitBuffer(asciiToBytes(string), buf, offset, length)\n}\n\nfunction base64Write (buf, string, offset, length) {\n return blitBuffer(base64ToBytes(string), buf, offset, length)\n}\n\nfunction ucs2Write (buf, string, offset, length) {\n return blitBuffer(utf16leToBytes(string, buf.length - offset), buf, offset, length)\n}\n\nBuffer.prototype.write = function write (string, offset, length, encoding) {\n // Buffer#write(string)\n if (offset === undefined) {\n encoding = 'utf8'\n length = this.length\n offset = 0\n // Buffer#write(string, encoding)\n } else if (length === undefined && typeof offset === 'string') {\n encoding = offset\n length = this.length\n offset = 0\n // Buffer#write(string, offset[, length][, encoding])\n } else if (isFinite(offset)) {\n offset = offset >>> 0\n if (isFinite(length)) {\n length = length >>> 0\n if (encoding === undefined) encoding = 'utf8'\n } else {\n encoding = length\n length = undefined\n }\n } else {\n throw new Error(\n 'Buffer.write(string, encoding, offset[, length]) is no longer supported'\n )\n }\n\n const remaining = this.length - offset\n if (length === undefined || length > remaining) length = remaining\n\n if ((string.length > 0 && (length < 0 || offset < 0)) || offset > this.length) {\n throw new RangeError('Attempt to write outside buffer bounds')\n }\n\n if (!encoding) encoding = 'utf8'\n\n let loweredCase = false\n for (;;) {\n switch (encoding) {\n case 'hex':\n return hexWrite(this, string, offset, length)\n\n case 'utf8':\n case 'utf-8':\n return utf8Write(this, string, offset, length)\n\n case 'ascii':\n case 'latin1':\n case 'binary':\n return asciiWrite(this, string, offset, length)\n\n case 'base64':\n // Warning: maxLength not taken into account in base64Write\n return base64Write(this, string, offset, length)\n\n case 'ucs2':\n case 'ucs-2':\n case 'utf16le':\n case 'utf-16le':\n return ucs2Write(this, string, offset, length)\n\n default:\n if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding)\n encoding = ('' + encoding).toLowerCase()\n loweredCase = true\n }\n }\n}\n\nBuffer.prototype.toJSON = function toJSON () {\n return {\n type: 'Buffer',\n data: Array.prototype.slice.call(this._arr || this, 0)\n }\n}\n\nfunction base64Slice (buf, start, end) {\n if (start === 0 && end === buf.length) {\n return base64.fromByteArray(buf)\n } else {\n return base64.fromByteArray(buf.slice(start, end))\n }\n}\n\nfunction utf8Slice (buf, start, end) {\n end = Math.min(buf.length, end)\n const res = []\n\n let i = start\n while (i < end) {\n const firstByte = buf[i]\n let codePoint = null\n let bytesPerSequence = (firstByte > 0xEF)\n ? 4\n : (firstByte > 0xDF)\n ? 3\n : (firstByte > 0xBF)\n ? 2\n : 1\n\n if (i + bytesPerSequence <= end) {\n let secondByte, thirdByte, fourthByte, tempCodePoint\n\n switch (bytesPerSequence) {\n case 1:\n if (firstByte < 0x80) {\n codePoint = firstByte\n }\n break\n case 2:\n secondByte = buf[i + 1]\n if ((secondByte & 0xC0) === 0x80) {\n tempCodePoint = (firstByte & 0x1F) << 0x6 | (secondByte & 0x3F)\n if (tempCodePoint > 0x7F) {\n codePoint = tempCodePoint\n }\n }\n break\n case 3:\n secondByte = buf[i + 1]\n thirdByte = buf[i + 2]\n if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80) {\n tempCodePoint = (firstByte & 0xF) << 0xC | (secondByte & 0x3F) << 0x6 | (thirdByte & 0x3F)\n if (tempCodePoint > 0x7FF && (tempCodePoint < 0xD800 || tempCodePoint > 0xDFFF)) {\n codePoint = tempCodePoint\n }\n }\n break\n case 4:\n secondByte = buf[i + 1]\n thirdByte = buf[i + 2]\n fourthByte = buf[i + 3]\n if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80 && (fourthByte & 0xC0) === 0x80) {\n tempCodePoint = (firstByte & 0xF) << 0x12 | (secondByte & 0x3F) << 0xC | (thirdByte & 0x3F) << 0x6 | (fourthByte & 0x3F)\n if (tempCodePoint > 0xFFFF && tempCodePoint < 0x110000) {\n codePoint = tempCodePoint\n }\n }\n }\n }\n\n if (codePoint === null) {\n // we did not generate a valid codePoint so insert a\n // replacement char (U+FFFD) and advance only 1 byte\n codePoint = 0xFFFD\n bytesPerSequence = 1\n } else if (codePoint > 0xFFFF) {\n // encode to utf16 (surrogate pair dance)\n codePoint -= 0x10000\n res.push(codePoint >>> 10 & 0x3FF | 0xD800)\n codePoint = 0xDC00 | codePoint & 0x3FF\n }\n\n res.push(codePoint)\n i += bytesPerSequence\n }\n\n return decodeCodePointsArray(res)\n}\n\n// Based on http://stackoverflow.com/a/22747272/680742, the browser with\n// the lowest limit is Chrome, with 0x10000 args.\n// We go 1 magnitude less, for safety\nconst MAX_ARGUMENTS_LENGTH = 0x1000\n\nfunction decodeCodePointsArray (codePoints) {\n const len = codePoints.length\n if (len <= MAX_ARGUMENTS_LENGTH) {\n return String.fromCharCode.apply(String, codePoints) // avoid extra slice()\n }\n\n // Decode in chunks to avoid \"call stack size exceeded\".\n let res = ''\n let i = 0\n while (i < len) {\n res += String.fromCharCode.apply(\n String,\n codePoints.slice(i, i += MAX_ARGUMENTS_LENGTH)\n )\n }\n return res\n}\n\nfunction asciiSlice (buf, start, end) {\n let ret = ''\n end = Math.min(buf.length, end)\n\n for (let i = start; i < end; ++i) {\n ret += String.fromCharCode(buf[i] & 0x7F)\n }\n return ret\n}\n\nfunction latin1Slice (buf, start, end) {\n let ret = ''\n end = Math.min(buf.length, end)\n\n for (let i = start; i < end; ++i) {\n ret += String.fromCharCode(buf[i])\n }\n return ret\n}\n\nfunction hexSlice (buf, start, end) {\n const len = buf.length\n\n if (!start || start < 0) start = 0\n if (!end || end < 0 || end > len) end = len\n\n let out = ''\n for (let i = start; i < end; ++i) {\n out += hexSliceLookupTable[buf[i]]\n }\n return out\n}\n\nfunction utf16leSlice (buf, start, end) {\n const bytes = buf.slice(start, end)\n let res = ''\n // If bytes.length is odd, the last 8 bits must be ignored (same as node.js)\n for (let i = 0; i < bytes.length - 1; i += 2) {\n res += String.fromCharCode(bytes[i] + (bytes[i + 1] * 256))\n }\n return res\n}\n\nBuffer.prototype.slice = function slice (start, end) {\n const len = this.length\n start = ~~start\n end = end === undefined ? len : ~~end\n\n if (start < 0) {\n start += len\n if (start < 0) start = 0\n } else if (start > len) {\n start = len\n }\n\n if (end < 0) {\n end += len\n if (end < 0) end = 0\n } else if (end > len) {\n end = len\n }\n\n if (end < start) end = start\n\n const newBuf = this.subarray(start, end)\n // Return an augmented `Uint8Array` instance\n Object.setPrototypeOf(newBuf, Buffer.prototype)\n\n return newBuf\n}\n\n/*\n * Need to make sure that buffer isn't trying to write out of bounds.\n */\nfunction checkOffset (offset, ext, length) {\n if ((offset % 1) !== 0 || offset < 0) throw new RangeError('offset is not uint')\n if (offset + ext > length) throw new RangeError('Trying to access beyond buffer length')\n}\n\nBuffer.prototype.readUintLE =\nBuffer.prototype.readUIntLE = function readUIntLE (offset, byteLength, noAssert) {\n offset = offset >>> 0\n byteLength = byteLength >>> 0\n if (!noAssert) checkOffset(offset, byteLength, this.length)\n\n let val = this[offset]\n let mul = 1\n let i = 0\n while (++i < byteLength && (mul *= 0x100)) {\n val += this[offset + i] * mul\n }\n\n return val\n}\n\nBuffer.prototype.readUintBE =\nBuffer.prototype.readUIntBE = function readUIntBE (offset, byteLength, noAssert) {\n offset = offset >>> 0\n byteLength = byteLength >>> 0\n if (!noAssert) {\n checkOffset(offset, byteLength, this.length)\n }\n\n let val = this[offset + --byteLength]\n let mul = 1\n while (byteLength > 0 && (mul *= 0x100)) {\n val += this[offset + --byteLength] * mul\n }\n\n return val\n}\n\nBuffer.prototype.readUint8 =\nBuffer.prototype.readUInt8 = function readUInt8 (offset, noAssert) {\n offset = offset >>> 0\n if (!noAssert) checkOffset(offset, 1, this.length)\n return this[offset]\n}\n\nBuffer.prototype.readUint16LE =\nBuffer.prototype.readUInt16LE = function readUInt16LE (offset, noAssert) {\n offset = offset >>> 0\n if (!noAssert) checkOffset(offset, 2, this.length)\n return this[offset] | (this[offset + 1] << 8)\n}\n\nBuffer.prototype.readUint16BE =\nBuffer.prototype.readUInt16BE = function readUInt16BE (offset, noAssert) {\n offset = offset >>> 0\n if (!noAssert) checkOffset(offset, 2, this.length)\n return (this[offset] << 8) | this[offset + 1]\n}\n\nBuffer.prototype.readUint32LE =\nBuffer.prototype.readUInt32LE = function readUInt32LE (offset, noAssert) {\n offset = offset >>> 0\n if (!noAssert) checkOffset(offset, 4, this.length)\n\n return ((this[offset]) |\n (this[offset + 1] << 8) |\n (this[offset + 2] << 16)) +\n (this[offset + 3] * 0x1000000)\n}\n\nBuffer.prototype.readUint32BE =\nBuffer.prototype.readUInt32BE = function readUInt32BE (offset, noAssert) {\n offset = offset >>> 0\n if (!noAssert) checkOffset(offset, 4, this.length)\n\n return (this[offset] * 0x1000000) +\n ((this[offset + 1] << 16) |\n (this[offset + 2] << 8) |\n this[offset + 3])\n}\n\nBuffer.prototype.readBigUInt64LE = defineBigIntMethod(function readBigUInt64LE (offset) {\n offset = offset >>> 0\n validateNumber(offset, 'offset')\n const first = this[offset]\n const last = this[offset + 7]\n if (first === undefined || last === undefined) {\n boundsError(offset, this.length - 8)\n }\n\n const lo = first +\n this[++offset] * 2 ** 8 +\n this[++offset] * 2 ** 16 +\n this[++offset] * 2 ** 24\n\n const hi = this[++offset] +\n this[++offset] * 2 ** 8 +\n this[++offset] * 2 ** 16 +\n last * 2 ** 24\n\n return BigInt(lo) + (BigInt(hi) << BigInt(32))\n})\n\nBuffer.prototype.readBigUInt64BE = defineBigIntMethod(function readBigUInt64BE (offset) {\n offset = offset >>> 0\n validateNumber(offset, 'offset')\n const first = this[offset]\n const last = this[offset + 7]\n if (first === undefined || last === undefined) {\n boundsError(offset, this.length - 8)\n }\n\n const hi = first * 2 ** 24 +\n this[++offset] * 2 ** 16 +\n this[++offset] * 2 ** 8 +\n this[++offset]\n\n const lo = this[++offset] * 2 ** 24 +\n this[++offset] * 2 ** 16 +\n this[++offset] * 2 ** 8 +\n last\n\n return (BigInt(hi) << BigInt(32)) + BigInt(lo)\n})\n\nBuffer.prototype.readIntLE = function readIntLE (offset, byteLength, noAssert) {\n offset = offset >>> 0\n byteLength = byteLength >>> 0\n if (!noAssert) checkOffset(offset, byteLength, this.length)\n\n let val = this[offset]\n let mul = 1\n let i = 0\n while (++i < byteLength && (mul *= 0x100)) {\n val += this[offset + i] * mul\n }\n mul *= 0x80\n\n if (val >= mul) val -= Math.pow(2, 8 * byteLength)\n\n return val\n}\n\nBuffer.prototype.readIntBE = function readIntBE (offset, byteLength, noAssert) {\n offset = offset >>> 0\n byteLength = byteLength >>> 0\n if (!noAssert) checkOffset(offset, byteLength, this.length)\n\n let i = byteLength\n let mul = 1\n let val = this[offset + --i]\n while (i > 0 && (mul *= 0x100)) {\n val += this[offset + --i] * mul\n }\n mul *= 0x80\n\n if (val >= mul) val -= Math.pow(2, 8 * byteLength)\n\n return val\n}\n\nBuffer.prototype.readInt8 = function readInt8 (offset, noAssert) {\n offset = offset >>> 0\n if (!noAssert) checkOffset(offset, 1, this.length)\n if (!(this[offset] & 0x80)) return (this[offset])\n return ((0xff - this[offset] + 1) * -1)\n}\n\nBuffer.prototype.readInt16LE = function readInt16LE (offset, noAssert) {\n offset = offset >>> 0\n if (!noAssert) checkOffset(offset, 2, this.length)\n const val = this[offset] | (this[offset + 1] << 8)\n return (val & 0x8000) ? val | 0xFFFF0000 : val\n}\n\nBuffer.prototype.readInt16BE = function readInt16BE (offset, noAssert) {\n offset = offset >>> 0\n if (!noAssert) checkOffset(offset, 2, this.length)\n const val = this[offset + 1] | (this[offset] << 8)\n return (val & 0x8000) ? val | 0xFFFF0000 : val\n}\n\nBuffer.prototype.readInt32LE = function readInt32LE (offset, noAssert) {\n offset = offset >>> 0\n if (!noAssert) checkOffset(offset, 4, this.length)\n\n return (this[offset]) |\n (this[offset + 1] << 8) |\n (this[offset + 2] << 16) |\n (this[offset + 3] << 24)\n}\n\nBuffer.prototype.readInt32BE = function readInt32BE (offset, noAssert) {\n offset = offset >>> 0\n if (!noAssert) checkOffset(offset, 4, this.length)\n\n return (this[offset] << 24) |\n (this[offset + 1] << 16) |\n (this[offset + 2] << 8) |\n (this[offset + 3])\n}\n\nBuffer.prototype.readBigInt64LE = defineBigIntMethod(function readBigInt64LE (offset) {\n offset = offset >>> 0\n validateNumber(offset, 'offset')\n const first = this[offset]\n const last = this[offset + 7]\n if (first === undefined || last === undefined) {\n boundsError(offset, this.length - 8)\n }\n\n const val = this[offset + 4] +\n this[offset + 5] * 2 ** 8 +\n this[offset + 6] * 2 ** 16 +\n (last << 24) // Overflow\n\n return (BigInt(val) << BigInt(32)) +\n BigInt(first +\n this[++offset] * 2 ** 8 +\n this[++offset] * 2 ** 16 +\n this[++offset] * 2 ** 24)\n})\n\nBuffer.prototype.readBigInt64BE = defineBigIntMethod(function readBigInt64BE (offset) {\n offset = offset >>> 0\n validateNumber(offset, 'offset')\n const first = this[offset]\n const last = this[offset + 7]\n if (first === undefined || last === undefined) {\n boundsError(offset, this.length - 8)\n }\n\n const val = (first << 24) + // Overflow\n this[++offset] * 2 ** 16 +\n this[++offset] * 2 ** 8 +\n this[++offset]\n\n return (BigInt(val) << BigInt(32)) +\n BigInt(this[++offset] * 2 ** 24 +\n this[++offset] * 2 ** 16 +\n this[++offset] * 2 ** 8 +\n last)\n})\n\nBuffer.prototype.readFloatLE = function readFloatLE (offset, noAssert) {\n offset = offset >>> 0\n if (!noAssert) checkOffset(offset, 4, this.length)\n return ieee754.read(this, offset, true, 23, 4)\n}\n\nBuffer.prototype.readFloatBE = function readFloatBE (offset, noAssert) {\n offset = offset >>> 0\n if (!noAssert) checkOffset(offset, 4, this.length)\n return ieee754.read(this, offset, false, 23, 4)\n}\n\nBuffer.prototype.readDoubleLE = function readDoubleLE (offset, noAssert) {\n offset = offset >>> 0\n if (!noAssert) checkOffset(offset, 8, this.length)\n return ieee754.read(this, offset, true, 52, 8)\n}\n\nBuffer.prototype.readDoubleBE = function readDoubleBE (offset, noAssert) {\n offset = offset >>> 0\n if (!noAssert) checkOffset(offset, 8, this.length)\n return ieee754.read(this, offset, false, 52, 8)\n}\n\nfunction checkInt (buf, value, offset, ext, max, min) {\n if (!Buffer.isBuffer(buf)) throw new TypeError('\"buffer\" argument must be a Buffer instance')\n if (value > max || value < min) throw new RangeError('\"value\" argument is out of bounds')\n if (offset + ext > buf.length) throw new RangeError('Index out of range')\n}\n\nBuffer.prototype.writeUintLE =\nBuffer.prototype.writeUIntLE = function writeUIntLE (value, offset, byteLength, noAssert) {\n value = +value\n offset = offset >>> 0\n byteLength = byteLength >>> 0\n if (!noAssert) {\n const maxBytes = Math.pow(2, 8 * byteLength) - 1\n checkInt(this, value, offset, byteLength, maxBytes, 0)\n }\n\n let mul = 1\n let i = 0\n this[offset] = value & 0xFF\n while (++i < byteLength && (mul *= 0x100)) {\n this[offset + i] = (value / mul) & 0xFF\n }\n\n return offset + byteLength\n}\n\nBuffer.prototype.writeUintBE =\nBuffer.prototype.writeUIntBE = function writeUIntBE (value, offset, byteLength, noAssert) {\n value = +value\n offset = offset >>> 0\n byteLength = byteLength >>> 0\n if (!noAssert) {\n const maxBytes = Math.pow(2, 8 * byteLength) - 1\n checkInt(this, value, offset, byteLength, maxBytes, 0)\n }\n\n let i = byteLength - 1\n let mul = 1\n this[offset + i] = value & 0xFF\n while (--i >= 0 && (mul *= 0x100)) {\n this[offset + i] = (value / mul) & 0xFF\n }\n\n return offset + byteLength\n}\n\nBuffer.prototype.writeUint8 =\nBuffer.prototype.writeUInt8 = function writeUInt8 (value, offset, noAssert) {\n value = +value\n offset = offset >>> 0\n if (!noAssert) checkInt(this, value, offset, 1, 0xff, 0)\n this[offset] = (value & 0xff)\n return offset + 1\n}\n\nBuffer.prototype.writeUint16LE =\nBuffer.prototype.writeUInt16LE = function writeUInt16LE (value, offset, noAssert) {\n value = +value\n offset = offset >>> 0\n if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0)\n this[offset] = (value & 0xff)\n this[offset + 1] = (value >>> 8)\n return offset + 2\n}\n\nBuffer.prototype.writeUint16BE =\nBuffer.prototype.writeUInt16BE = function writeUInt16BE (value, offset, noAssert) {\n value = +value\n offset = offset >>> 0\n if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0)\n this[offset] = (value >>> 8)\n this[offset + 1] = (value & 0xff)\n return offset + 2\n}\n\nBuffer.prototype.writeUint32LE =\nBuffer.prototype.writeUInt32LE = function writeUInt32LE (value, offset, noAssert) {\n value = +value\n offset = offset >>> 0\n if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0)\n this[offset + 3] = (value >>> 24)\n this[offset + 2] = (value >>> 16)\n this[offset + 1] = (value >>> 8)\n this[offset] = (value & 0xff)\n return offset + 4\n}\n\nBuffer.prototype.writeUint32BE =\nBuffer.prototype.writeUInt32BE = function writeUInt32BE (value, offset, noAssert) {\n value = +value\n offset = offset >>> 0\n if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0)\n this[offset] = (value >>> 24)\n this[offset + 1] = (value >>> 16)\n this[offset + 2] = (value >>> 8)\n this[offset + 3] = (value & 0xff)\n return offset + 4\n}\n\nfunction wrtBigUInt64LE (buf, value, offset, min, max) {\n checkIntBI(value, min, max, buf, offset, 7)\n\n let lo = Number(value & BigInt(0xffffffff))\n buf[offset++] = lo\n lo = lo >> 8\n buf[offset++] = lo\n lo = lo >> 8\n buf[offset++] = lo\n lo = lo >> 8\n buf[offset++] = lo\n let hi = Number(value >> BigInt(32) & BigInt(0xffffffff))\n buf[offset++] = hi\n hi = hi >> 8\n buf[offset++] = hi\n hi = hi >> 8\n buf[offset++] = hi\n hi = hi >> 8\n buf[offset++] = hi\n return offset\n}\n\nfunction wrtBigUInt64BE (buf, value, offset, min, max) {\n checkIntBI(value, min, max, buf, offset, 7)\n\n let lo = Number(value & BigInt(0xffffffff))\n buf[offset + 7] = lo\n lo = lo >> 8\n buf[offset + 6] = lo\n lo = lo >> 8\n buf[offset + 5] = lo\n lo = lo >> 8\n buf[offset + 4] = lo\n let hi = Number(value >> BigInt(32) & BigInt(0xffffffff))\n buf[offset + 3] = hi\n hi = hi >> 8\n buf[offset + 2] = hi\n hi = hi >> 8\n buf[offset + 1] = hi\n hi = hi >> 8\n buf[offset] = hi\n return offset + 8\n}\n\nBuffer.prototype.writeBigUInt64LE = defineBigIntMethod(function writeBigUInt64LE (value, offset = 0) {\n return wrtBigUInt64LE(this, value, offset, BigInt(0), BigInt('0xffffffffffffffff'))\n})\n\nBuffer.prototype.writeBigUInt64BE = defineBigIntMethod(function writeBigUInt64BE (value, offset = 0) {\n return wrtBigUInt64BE(this, value, offset, BigInt(0), BigInt('0xffffffffffffffff'))\n})\n\nBuffer.prototype.writeIntLE = function writeIntLE (value, offset, byteLength, noAssert) {\n value = +value\n offset = offset >>> 0\n if (!noAssert) {\n const limit = Math.pow(2, (8 * byteLength) - 1)\n\n checkInt(this, value, offset, byteLength, limit - 1, -limit)\n }\n\n let i = 0\n let mul = 1\n let sub = 0\n this[offset] = value & 0xFF\n while (++i < byteLength && (mul *= 0x100)) {\n if (value < 0 && sub === 0 && this[offset + i - 1] !== 0) {\n sub = 1\n }\n this[offset + i] = ((value / mul) >> 0) - sub & 0xFF\n }\n\n return offset + byteLength\n}\n\nBuffer.prototype.writeIntBE = function writeIntBE (value, offset, byteLength, noAssert) {\n value = +value\n offset = offset >>> 0\n if (!noAssert) {\n const limit = Math.pow(2, (8 * byteLength) - 1)\n\n checkInt(this, value, offset, byteLength, limit - 1, -limit)\n }\n\n let i = byteLength - 1\n let mul = 1\n let sub = 0\n this[offset + i] = value & 0xFF\n while (--i >= 0 && (mul *= 0x100)) {\n if (value < 0 && sub === 0 && this[offset + i + 1] !== 0) {\n sub = 1\n }\n this[offset + i] = ((value / mul) >> 0) - sub & 0xFF\n }\n\n return offset + byteLength\n}\n\nBuffer.prototype.writeInt8 = function writeInt8 (value, offset, noAssert) {\n value = +value\n offset = offset >>> 0\n if (!noAssert) checkInt(this, value, offset, 1, 0x7f, -0x80)\n if (value < 0) value = 0xff + value + 1\n this[offset] = (value & 0xff)\n return offset + 1\n}\n\nBuffer.prototype.writeInt16LE = function writeInt16LE (value, offset, noAssert) {\n value = +value\n offset = offset >>> 0\n if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000)\n this[offset] = (value & 0xff)\n this[offset + 1] = (value >>> 8)\n return offset + 2\n}\n\nBuffer.prototype.writeInt16BE = function writeInt16BE (value, offset, noAssert) {\n value = +value\n offset = offset >>> 0\n if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000)\n this[offset] = (value >>> 8)\n this[offset + 1] = (value & 0xff)\n return offset + 2\n}\n\nBuffer.prototype.writeInt32LE = function writeInt32LE (value, offset, noAssert) {\n value = +value\n offset = offset >>> 0\n if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000)\n this[offset] = (value & 0xff)\n this[offset + 1] = (value >>> 8)\n this[offset + 2] = (value >>> 16)\n this[offset + 3] = (value >>> 24)\n return offset + 4\n}\n\nBuffer.prototype.writeInt32BE = function writeInt32BE (value, offset, noAssert) {\n value = +value\n offset = offset >>> 0\n if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000)\n if (value < 0) value = 0xffffffff + value + 1\n this[offset] = (value >>> 24)\n this[offset + 1] = (value >>> 16)\n this[offset + 2] = (value >>> 8)\n this[offset + 3] = (value & 0xff)\n return offset + 4\n}\n\nBuffer.prototype.writeBigInt64LE = defineBigIntMethod(function writeBigInt64LE (value, offset = 0) {\n return wrtBigUInt64LE(this, value, offset, -BigInt('0x8000000000000000'), BigInt('0x7fffffffffffffff'))\n})\n\nBuffer.prototype.writeBigInt64BE = defineBigIntMethod(function writeBigInt64BE (value, offset = 0) {\n return wrtBigUInt64BE(this, value, offset, -BigInt('0x8000000000000000'), BigInt('0x7fffffffffffffff'))\n})\n\nfunction checkIEEE754 (buf, value, offset, ext, max, min) {\n if (offset + ext > buf.length) throw new RangeError('Index out of range')\n if (offset < 0) throw new RangeError('Index out of range')\n}\n\nfunction writeFloat (buf, value, offset, littleEndian, noAssert) {\n value = +value\n offset = offset >>> 0\n if (!noAssert) {\n checkIEEE754(buf, value, offset, 4, 3.4028234663852886e+38, -3.4028234663852886e+38)\n }\n ieee754.write(buf, value, offset, littleEndian, 23, 4)\n return offset + 4\n}\n\nBuffer.prototype.writeFloatLE = function writeFloatLE (value, offset, noAssert) {\n return writeFloat(this, value, offset, true, noAssert)\n}\n\nBuffer.prototype.writeFloatBE = function writeFloatBE (value, offset, noAssert) {\n return writeFloat(this, value, offset, false, noAssert)\n}\n\nfunction writeDouble (buf, value, offset, littleEndian, noAssert) {\n value = +value\n offset = offset >>> 0\n if (!noAssert) {\n checkIEEE754(buf, value, offset, 8, 1.7976931348623157E+308, -1.7976931348623157E+308)\n }\n ieee754.write(buf, value, offset, littleEndian, 52, 8)\n return offset + 8\n}\n\nBuffer.prototype.writeDoubleLE = function writeDoubleLE (value, offset, noAssert) {\n return writeDouble(this, value, offset, true, noAssert)\n}\n\nBuffer.prototype.writeDoubleBE = function writeDoubleBE (value, offset, noAssert) {\n return writeDouble(this, value, offset, false, noAssert)\n}\n\n// copy(targetBuffer, targetStart=0, sourceStart=0, sourceEnd=buffer.length)\nBuffer.prototype.copy = function copy (target, targetStart, start, end) {\n if (!Buffer.isBuffer(target)) throw new TypeError('argument should be a Buffer')\n if (!start) start = 0\n if (!end && end !== 0) end = this.length\n if (targetStart >= target.length) targetStart = target.length\n if (!targetStart) targetStart = 0\n if (end > 0 && end < start) end = start\n\n // Copy 0 bytes; we're done\n if (end === start) return 0\n if (target.length === 0 || this.length === 0) return 0\n\n // Fatal error conditions\n if (targetStart < 0) {\n throw new RangeError('targetStart out of bounds')\n }\n if (start < 0 || start >= this.length) throw new RangeError('Index out of range')\n if (end < 0) throw new RangeError('sourceEnd out of bounds')\n\n // Are we oob?\n if (end > this.length) end = this.length\n if (target.length - targetStart < end - start) {\n end = target.length - targetStart + start\n }\n\n const len = end - start\n\n if (this === target && typeof Uint8Array.prototype.copyWithin === 'function') {\n // Use built-in when available, missing from IE11\n this.copyWithin(targetStart, start, end)\n } else {\n Uint8Array.prototype.set.call(\n target,\n this.subarray(start, end),\n targetStart\n )\n }\n\n return len\n}\n\n// Usage:\n// buffer.fill(number[, offset[, end]])\n// buffer.fill(buffer[, offset[, end]])\n// buffer.fill(string[, offset[, end]][, encoding])\nBuffer.prototype.fill = function fill (val, start, end, encoding) {\n // Handle string cases:\n if (typeof val === 'string') {\n if (typeof start === 'string') {\n encoding = start\n start = 0\n end = this.length\n } else if (typeof end === 'string') {\n encoding = end\n end = this.length\n }\n if (encoding !== undefined && typeof encoding !== 'string') {\n throw new TypeError('encoding must be a string')\n }\n if (typeof encoding === 'string' && !Buffer.isEncoding(encoding)) {\n throw new TypeError('Unknown encoding: ' + encoding)\n }\n if (val.length === 1) {\n const code = val.charCodeAt(0)\n if ((encoding === 'utf8' && code < 128) ||\n encoding === 'latin1') {\n // Fast path: If `val` fits into a single byte, use that numeric value.\n val = code\n }\n }\n } else if (typeof val === 'number') {\n val = val & 255\n } else if (typeof val === 'boolean') {\n val = Number(val)\n }\n\n // Invalid ranges are not set to a default, so can range check early.\n if (start < 0 || this.length < start || this.length < end) {\n throw new RangeError('Out of range index')\n }\n\n if (end <= start) {\n return this\n }\n\n start = start >>> 0\n end = end === undefined ? this.length : end >>> 0\n\n if (!val) val = 0\n\n let i\n if (typeof val === 'number') {\n for (i = start; i < end; ++i) {\n this[i] = val\n }\n } else {\n const bytes = Buffer.isBuffer(val)\n ? val\n : Buffer.from(val, encoding)\n const len = bytes.length\n if (len === 0) {\n throw new TypeError('The value \"' + val +\n '\" is invalid for argument \"value\"')\n }\n for (i = 0; i < end - start; ++i) {\n this[i + start] = bytes[i % len]\n }\n }\n\n return this\n}\n\n// CUSTOM ERRORS\n// =============\n\n// Simplified versions from Node, changed for Buffer-only usage\nconst errors = {}\nfunction E (sym, getMessage, Base) {\n errors[sym] = class NodeError extends Base {\n constructor () {\n super()\n\n Object.defineProperty(this, 'message', {\n value: getMessage.apply(this, arguments),\n writable: true,\n configurable: true\n })\n\n // Add the error code to the name to include it in the stack trace.\n this.name = `${this.name} [${sym}]`\n // Access the stack to generate the error message including the error code\n // from the name.\n this.stack // eslint-disable-line no-unused-expressions\n // Reset the name to the actual name.\n delete this.name\n }\n\n get code () {\n return sym\n }\n\n set code (value) {\n Object.defineProperty(this, 'code', {\n configurable: true,\n enumerable: true,\n value,\n writable: true\n })\n }\n\n toString () {\n return `${this.name} [${sym}]: ${this.message}`\n }\n }\n}\n\nE('ERR_BUFFER_OUT_OF_BOUNDS',\n function (name) {\n if (name) {\n return `${name} is outside of buffer bounds`\n }\n\n return 'Attempt to access memory outside buffer bounds'\n }, RangeError)\nE('ERR_INVALID_ARG_TYPE',\n function (name, actual) {\n return `The \"${name}\" argument must be of type number. Received type ${typeof actual}`\n }, TypeError)\nE('ERR_OUT_OF_RANGE',\n function (str, range, input) {\n let msg = `The value of \"${str}\" is out of range.`\n let received = input\n if (Number.isInteger(input) && Math.abs(input) > 2 ** 32) {\n received = addNumericalSeparator(String(input))\n } else if (typeof input === 'bigint') {\n received = String(input)\n if (input > BigInt(2) ** BigInt(32) || input < -(BigInt(2) ** BigInt(32))) {\n received = addNumericalSeparator(received)\n }\n received += 'n'\n }\n msg += ` It must be ${range}. Received ${received}`\n return msg\n }, RangeError)\n\nfunction addNumericalSeparator (val) {\n let res = ''\n let i = val.length\n const start = val[0] === '-' ? 1 : 0\n for (; i >= start + 4; i -= 3) {\n res = `_${val.slice(i - 3, i)}${res}`\n }\n return `${val.slice(0, i)}${res}`\n}\n\n// CHECK FUNCTIONS\n// ===============\n\nfunction checkBounds (buf, offset, byteLength) {\n validateNumber(offset, 'offset')\n if (buf[offset] === undefined || buf[offset + byteLength] === undefined) {\n boundsError(offset, buf.length - (byteLength + 1))\n }\n}\n\nfunction checkIntBI (value, min, max, buf, offset, byteLength) {\n if (value > max || value < min) {\n const n = typeof min === 'bigint' ? 'n' : ''\n let range\n if (byteLength > 3) {\n if (min === 0 || min === BigInt(0)) {\n range = `>= 0${n} and < 2${n} ** ${(byteLength + 1) * 8}${n}`\n } else {\n range = `>= -(2${n} ** ${(byteLength + 1) * 8 - 1}${n}) and < 2 ** ` +\n `${(byteLength + 1) * 8 - 1}${n}`\n }\n } else {\n range = `>= ${min}${n} and <= ${max}${n}`\n }\n throw new errors.ERR_OUT_OF_RANGE('value', range, value)\n }\n checkBounds(buf, offset, byteLength)\n}\n\nfunction validateNumber (value, name) {\n if (typeof value !== 'number') {\n throw new errors.ERR_INVALID_ARG_TYPE(name, 'number', value)\n }\n}\n\nfunction boundsError (value, length, type) {\n if (Math.floor(value) !== value) {\n validateNumber(value, type)\n throw new errors.ERR_OUT_OF_RANGE(type || 'offset', 'an integer', value)\n }\n\n if (length < 0) {\n throw new errors.ERR_BUFFER_OUT_OF_BOUNDS()\n }\n\n throw new errors.ERR_OUT_OF_RANGE(type || 'offset',\n `>= ${type ? 1 : 0} and <= ${length}`,\n value)\n}\n\n// HELPER FUNCTIONS\n// ================\n\nconst INVALID_BASE64_RE = /[^+/0-9A-Za-z-_]/g\n\nfunction base64clean (str) {\n // Node takes equal signs as end of the Base64 encoding\n str = str.split('=')[0]\n // Node strips out invalid characters like \\n and \\t from the string, base64-js does not\n str = str.trim().replace(INVALID_BASE64_RE, '')\n // Node converts strings with length < 2 to ''\n if (str.length < 2) return ''\n // Node allows for non-padded base64 strings (missing trailing ===), base64-js does not\n while (str.length % 4 !== 0) {\n str = str + '='\n }\n return str\n}\n\nfunction utf8ToBytes (string, units) {\n units = units || Infinity\n let codePoint\n const length = string.length\n let leadSurrogate = null\n const bytes = []\n\n for (let i = 0; i < length; ++i) {\n codePoint = string.charCodeAt(i)\n\n // is surrogate component\n if (codePoint > 0xD7FF && codePoint < 0xE000) {\n // last char was a lead\n if (!leadSurrogate) {\n // no lead yet\n if (codePoint > 0xDBFF) {\n // unexpected trail\n if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD)\n continue\n } else if (i + 1 === length) {\n // unpaired lead\n if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD)\n continue\n }\n\n // valid lead\n leadSurrogate = codePoint\n\n continue\n }\n\n // 2 leads in a row\n if (codePoint < 0xDC00) {\n if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD)\n leadSurrogate = codePoint\n continue\n }\n\n // valid surrogate pair\n codePoint = (leadSurrogate - 0xD800 << 10 | codePoint - 0xDC00) + 0x10000\n } else if (leadSurrogate) {\n // valid bmp char, but last char was a lead\n if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD)\n }\n\n leadSurrogate = null\n\n // encode utf8\n if (codePoint < 0x80) {\n if ((units -= 1) < 0) break\n bytes.push(codePoint)\n } else if (codePoint < 0x800) {\n if ((units -= 2) < 0) break\n bytes.push(\n codePoint >> 0x6 | 0xC0,\n codePoint & 0x3F | 0x80\n )\n } else if (codePoint < 0x10000) {\n if ((units -= 3) < 0) break\n bytes.push(\n codePoint >> 0xC | 0xE0,\n codePoint >> 0x6 & 0x3F | 0x80,\n codePoint & 0x3F | 0x80\n )\n } else if (codePoint < 0x110000) {\n if ((units -= 4) < 0) break\n bytes.push(\n codePoint >> 0x12 | 0xF0,\n codePoint >> 0xC & 0x3F | 0x80,\n codePoint >> 0x6 & 0x3F | 0x80,\n codePoint & 0x3F | 0x80\n )\n } else {\n throw new Error('Invalid code point')\n }\n }\n\n return bytes\n}\n\nfunction asciiToBytes (str) {\n const byteArray = []\n for (let i = 0; i < str.length; ++i) {\n // Node's code seems to be doing this and not & 0x7F..\n byteArray.push(str.charCodeAt(i) & 0xFF)\n }\n return byteArray\n}\n\nfunction utf16leToBytes (str, units) {\n let c, hi, lo\n const byteArray = []\n for (let i = 0; i < str.length; ++i) {\n if ((units -= 2) < 0) break\n\n c = str.charCodeAt(i)\n hi = c >> 8\n lo = c % 256\n byteArray.push(lo)\n byteArray.push(hi)\n }\n\n return byteArray\n}\n\nfunction base64ToBytes (str) {\n return base64.toByteArray(base64clean(str))\n}\n\nfunction blitBuffer (src, dst, offset, length) {\n let i\n for (i = 0; i < length; ++i) {\n if ((i + offset >= dst.length) || (i >= src.length)) break\n dst[i + offset] = src[i]\n }\n return i\n}\n\n// ArrayBuffer or Uint8Array objects from other contexts (i.e. iframes) do not pass\n// the `instanceof` check but they should be treated as of that type.\n// See: https://github.com/feross/buffer/issues/166\nfunction isInstance (obj, type) {\n return obj instanceof type ||\n (obj != null && obj.constructor != null && obj.constructor.name != null &&\n obj.constructor.name === type.name)\n}\nfunction numberIsNaN (obj) {\n // For IE11 support\n return obj !== obj // eslint-disable-line no-self-compare\n}\n\n// Create lookup table for `toString('hex')`\n// See: https://github.com/feross/buffer/issues/219\nconst hexSliceLookupTable = (function () {\n const alphabet = '0123456789abcdef'\n const table = new Array(256)\n for (let i = 0; i < 16; ++i) {\n const i16 = i * 16\n for (let j = 0; j < 16; ++j) {\n table[i16 + j] = alphabet[i] + alphabet[j]\n }\n }\n return table\n})()\n\n// Return not function with Error if BigInt not supported\nfunction defineBigIntMethod (fn) {\n return typeof BigInt === 'undefined' ? BufferBigIntNotDefined : fn\n}\n\nfunction BufferBigIntNotDefined () {\n throw new Error('BigInt not supported')\n}\n","require('../../modules/es.object.define-property');\nvar path = require('../../internals/path');\n\nvar Object = path.Object;\n\nvar defineProperty = module.exports = function defineProperty(it, key, desc) {\n return Object.defineProperty(it, key, desc);\n};\n\nif (Object.defineProperty.sham) defineProperty.sham = true;\n","var isCallable = require('../internals/is-callable');\nvar tryToString = require('../internals/try-to-string');\n\nvar $TypeError = TypeError;\n\n// `Assert: IsCallable(argument) is true`\nmodule.exports = function (argument) {\n if (isCallable(argument)) return argument;\n throw $TypeError(tryToString(argument) + ' is not a function');\n};\n","var isObject = require('../internals/is-object');\n\nvar $String = String;\nvar $TypeError = TypeError;\n\n// `Assert: Type(argument) is Object`\nmodule.exports = function (argument) {\n if (isObject(argument)) return argument;\n throw $TypeError($String(argument) + ' is not an object');\n};\n","var uncurryThis = require('../internals/function-uncurry-this');\n\nvar toString = uncurryThis({}.toString);\nvar stringSlice = uncurryThis(''.slice);\n\nmodule.exports = function (it) {\n return stringSlice(toString(it), 8, -1);\n};\n","var DESCRIPTORS = require('../internals/descriptors');\nvar definePropertyModule = require('../internals/object-define-property');\nvar createPropertyDescriptor = require('../internals/create-property-descriptor');\n\nmodule.exports = DESCRIPTORS ? function (object, key, value) {\n return definePropertyModule.f(object, key, createPropertyDescriptor(1, value));\n} : function (object, key, value) {\n object[key] = value;\n return object;\n};\n","module.exports = function (bitmap, value) {\n return {\n enumerable: !(bitmap & 1),\n configurable: !(bitmap & 2),\n writable: !(bitmap & 4),\n value: value\n };\n};\n","var global = require('../internals/global');\n\n// eslint-disable-next-line es/no-object-defineproperty -- safe\nvar defineProperty = Object.defineProperty;\n\nmodule.exports = function (key, value) {\n try {\n defineProperty(global, key, { value: value, configurable: true, writable: true });\n } catch (error) {\n global[key] = value;\n } return value;\n};\n","var fails = require('../internals/fails');\n\n// Detect IE8's incomplete defineProperty implementation\nmodule.exports = !fails(function () {\n // eslint-disable-next-line es/no-object-defineproperty -- required for testing\n return Object.defineProperty({}, 1, { get: function () { return 7; } })[1] != 7;\n});\n","var documentAll = typeof document == 'object' && document.all;\n\n// https://tc39.es/ecma262/#sec-IsHTMLDDA-internal-slot\n// eslint-disable-next-line unicorn/no-typeof-undefined -- required for testing\nvar IS_HTMLDDA = typeof documentAll == 'undefined' && documentAll !== undefined;\n\nmodule.exports = {\n all: documentAll,\n IS_HTMLDDA: IS_HTMLDDA\n};\n","var global = require('../internals/global');\nvar isObject = require('../internals/is-object');\n\nvar document = global.document;\n// typeof document.createElement is 'object' in old IE\nvar EXISTS = isObject(document) && isObject(document.createElement);\n\nmodule.exports = function (it) {\n return EXISTS ? document.createElement(it) : {};\n};\n","module.exports = typeof navigator != 'undefined' && String(navigator.userAgent) || '';\n","var global = require('../internals/global');\nvar userAgent = require('../internals/engine-user-agent');\n\nvar process = global.process;\nvar Deno = global.Deno;\nvar versions = process && process.versions || Deno && Deno.version;\nvar v8 = versions && versions.v8;\nvar match, version;\n\nif (v8) {\n match = v8.split('.');\n // in old Chrome, versions of V8 isn't V8 = Chrome / 10\n // but their correct versions are not interesting for us\n version = match[0] > 0 && match[0] < 4 ? 1 : +(match[0] + match[1]);\n}\n\n// BrowserFS NodeJS `process` polyfill incorrectly set `.v8` to `0.0`\n// so check `userAgent` even if `.v8` exists, but 0\nif (!version && userAgent) {\n match = userAgent.match(/Edge\\/(\\d+)/);\n if (!match || match[1] >= 74) {\n match = userAgent.match(/Chrome\\/(\\d+)/);\n if (match) version = +match[1];\n }\n}\n\nmodule.exports = version;\n","'use strict';\nvar global = require('../internals/global');\nvar apply = require('../internals/function-apply');\nvar uncurryThis = require('../internals/function-uncurry-this-clause');\nvar isCallable = require('../internals/is-callable');\nvar getOwnPropertyDescriptor = require('../internals/object-get-own-property-descriptor').f;\nvar isForced = require('../internals/is-forced');\nvar path = require('../internals/path');\nvar bind = require('../internals/function-bind-context');\nvar createNonEnumerableProperty = require('../internals/create-non-enumerable-property');\nvar hasOwn = require('../internals/has-own-property');\n\nvar wrapConstructor = function (NativeConstructor) {\n var Wrapper = function (a, b, c) {\n if (this instanceof Wrapper) {\n switch (arguments.length) {\n case 0: return new NativeConstructor();\n case 1: return new NativeConstructor(a);\n case 2: return new NativeConstructor(a, b);\n } return new NativeConstructor(a, b, c);\n } return apply(NativeConstructor, this, arguments);\n };\n Wrapper.prototype = NativeConstructor.prototype;\n return Wrapper;\n};\n\n/*\n options.target - name of the target object\n options.global - target is the global object\n options.stat - export as static methods of target\n options.proto - export as prototype methods of target\n options.real - real prototype method for the `pure` version\n options.forced - export even if the native feature is available\n options.bind - bind methods to the target, required for the `pure` version\n options.wrap - wrap constructors to preventing global pollution, required for the `pure` version\n options.unsafe - use the simple assignment of property instead of delete + defineProperty\n options.sham - add a flag to not completely full polyfills\n options.enumerable - export as enumerable property\n options.dontCallGetSet - prevent calling a getter on target\n options.name - the .name of the function if it does not match the key\n*/\nmodule.exports = function (options, source) {\n var TARGET = options.target;\n var GLOBAL = options.global;\n var STATIC = options.stat;\n var PROTO = options.proto;\n\n var nativeSource = GLOBAL ? global : STATIC ? global[TARGET] : (global[TARGET] || {}).prototype;\n\n var target = GLOBAL ? path : path[TARGET] || createNonEnumerableProperty(path, TARGET, {})[TARGET];\n var targetPrototype = target.prototype;\n\n var FORCED, USE_NATIVE, VIRTUAL_PROTOTYPE;\n var key, sourceProperty, targetProperty, nativeProperty, resultProperty, descriptor;\n\n for (key in source) {\n FORCED = isForced(GLOBAL ? key : TARGET + (STATIC ? '.' : '#') + key, options.forced);\n // contains in native\n USE_NATIVE = !FORCED && nativeSource && hasOwn(nativeSource, key);\n\n targetProperty = target[key];\n\n if (USE_NATIVE) if (options.dontCallGetSet) {\n descriptor = getOwnPropertyDescriptor(nativeSource, key);\n nativeProperty = descriptor && descriptor.value;\n } else nativeProperty = nativeSource[key];\n\n // export native or implementation\n sourceProperty = (USE_NATIVE && nativeProperty) ? nativeProperty : source[key];\n\n if (USE_NATIVE && typeof targetProperty == typeof sourceProperty) continue;\n\n // bind methods to global for calling from export context\n if (options.bind && USE_NATIVE) resultProperty = bind(sourceProperty, global);\n // wrap global constructors for prevent changes in this version\n else if (options.wrap && USE_NATIVE) resultProperty = wrapConstructor(sourceProperty);\n // make static versions for prototype methods\n else if (PROTO && isCallable(sourceProperty)) resultProperty = uncurryThis(sourceProperty);\n // default case\n else resultProperty = sourceProperty;\n\n // add a flag to not completely full polyfills\n if (options.sham || (sourceProperty && sourceProperty.sham) || (targetProperty && targetProperty.sham)) {\n createNonEnumerableProperty(resultProperty, 'sham', true);\n }\n\n createNonEnumerableProperty(target, key, resultProperty);\n\n if (PROTO) {\n VIRTUAL_PROTOTYPE = TARGET + 'Prototype';\n if (!hasOwn(path, VIRTUAL_PROTOTYPE)) {\n createNonEnumerableProperty(path, VIRTUAL_PROTOTYPE, {});\n }\n // export virtual prototype methods\n createNonEnumerableProperty(path[VIRTUAL_PROTOTYPE], key, sourceProperty);\n // export real prototype methods\n if (options.real && targetPrototype && (FORCED || !targetPrototype[key])) {\n createNonEnumerableProperty(targetPrototype, key, sourceProperty);\n }\n }\n }\n};\n","module.exports = function (exec) {\n try {\n return !!exec();\n } catch (error) {\n return true;\n }\n};\n","var NATIVE_BIND = require('../internals/function-bind-native');\n\nvar FunctionPrototype = Function.prototype;\nvar apply = FunctionPrototype.apply;\nvar call = FunctionPrototype.call;\n\n// eslint-disable-next-line es/no-reflect -- safe\nmodule.exports = typeof Reflect == 'object' && Reflect.apply || (NATIVE_BIND ? call.bind(apply) : function () {\n return call.apply(apply, arguments);\n});\n","var uncurryThis = require('../internals/function-uncurry-this-clause');\nvar aCallable = require('../internals/a-callable');\nvar NATIVE_BIND = require('../internals/function-bind-native');\n\nvar bind = uncurryThis(uncurryThis.bind);\n\n// optional / simple context binding\nmodule.exports = function (fn, that) {\n aCallable(fn);\n return that === undefined ? fn : NATIVE_BIND ? bind(fn, that) : function (/* ...args */) {\n return fn.apply(that, arguments);\n };\n};\n","var fails = require('../internals/fails');\n\nmodule.exports = !fails(function () {\n // eslint-disable-next-line es/no-function-prototype-bind -- safe\n var test = (function () { /* empty */ }).bind();\n // eslint-disable-next-line no-prototype-builtins -- safe\n return typeof test != 'function' || test.hasOwnProperty('prototype');\n});\n","var NATIVE_BIND = require('../internals/function-bind-native');\n\nvar call = Function.prototype.call;\n\nmodule.exports = NATIVE_BIND ? call.bind(call) : function () {\n return call.apply(call, arguments);\n};\n","var classofRaw = require('../internals/classof-raw');\nvar uncurryThis = require('../internals/function-uncurry-this');\n\nmodule.exports = function (fn) {\n // Nashorn bug:\n // https://github.com/zloirock/core-js/issues/1128\n // https://github.com/zloirock/core-js/issues/1130\n if (classofRaw(fn) === 'Function') return uncurryThis(fn);\n};\n","var NATIVE_BIND = require('../internals/function-bind-native');\n\nvar FunctionPrototype = Function.prototype;\nvar call = FunctionPrototype.call;\nvar uncurryThisWithBind = NATIVE_BIND && FunctionPrototype.bind.bind(call, call);\n\nmodule.exports = NATIVE_BIND ? uncurryThisWithBind : function (fn) {\n return function () {\n return call.apply(fn, arguments);\n };\n};\n","var path = require('../internals/path');\nvar global = require('../internals/global');\nvar isCallable = require('../internals/is-callable');\n\nvar aFunction = function (variable) {\n return isCallable(variable) ? variable : undefined;\n};\n\nmodule.exports = function (namespace, method) {\n return arguments.length < 2 ? aFunction(path[namespace]) || aFunction(global[namespace])\n : path[namespace] && path[namespace][method] || global[namespace] && global[namespace][method];\n};\n","var aCallable = require('../internals/a-callable');\nvar isNullOrUndefined = require('../internals/is-null-or-undefined');\n\n// `GetMethod` abstract operation\n// https://tc39.es/ecma262/#sec-getmethod\nmodule.exports = function (V, P) {\n var func = V[P];\n return isNullOrUndefined(func) ? undefined : aCallable(func);\n};\n","var check = function (it) {\n return it && it.Math == Math && it;\n};\n\n// https://github.com/zloirock/core-js/issues/86#issuecomment-115759028\nmodule.exports =\n // eslint-disable-next-line es/no-global-this -- safe\n check(typeof globalThis == 'object' && globalThis) ||\n check(typeof window == 'object' && window) ||\n // eslint-disable-next-line no-restricted-globals -- safe\n check(typeof self == 'object' && self) ||\n check(typeof global == 'object' && global) ||\n // eslint-disable-next-line no-new-func -- fallback\n (function () { return this; })() || this || Function('return this')();\n","var uncurryThis = require('../internals/function-uncurry-this');\nvar toObject = require('../internals/to-object');\n\nvar hasOwnProperty = uncurryThis({}.hasOwnProperty);\n\n// `HasOwnProperty` abstract operation\n// https://tc39.es/ecma262/#sec-hasownproperty\n// eslint-disable-next-line es/no-object-hasown -- safe\nmodule.exports = Object.hasOwn || function hasOwn(it, key) {\n return hasOwnProperty(toObject(it), key);\n};\n","var DESCRIPTORS = require('../internals/descriptors');\nvar fails = require('../internals/fails');\nvar createElement = require('../internals/document-create-element');\n\n// Thanks to IE8 for its funny defineProperty\nmodule.exports = !DESCRIPTORS && !fails(function () {\n // eslint-disable-next-line es/no-object-defineproperty -- required for testing\n return Object.defineProperty(createElement('div'), 'a', {\n get: function () { return 7; }\n }).a != 7;\n});\n","var uncurryThis = require('../internals/function-uncurry-this');\nvar fails = require('../internals/fails');\nvar classof = require('../internals/classof-raw');\n\nvar $Object = Object;\nvar split = uncurryThis(''.split);\n\n// fallback for non-array-like ES3 and non-enumerable old V8 strings\nmodule.exports = fails(function () {\n // throws an error in rhino, see https://github.com/mozilla/rhino/issues/346\n // eslint-disable-next-line no-prototype-builtins -- safe\n return !$Object('z').propertyIsEnumerable(0);\n}) ? function (it) {\n return classof(it) == 'String' ? split(it, '') : $Object(it);\n} : $Object;\n","var $documentAll = require('../internals/document-all');\n\nvar documentAll = $documentAll.all;\n\n// `IsCallable` abstract operation\n// https://tc39.es/ecma262/#sec-iscallable\nmodule.exports = $documentAll.IS_HTMLDDA ? function (argument) {\n return typeof argument == 'function' || argument === documentAll;\n} : function (argument) {\n return typeof argument == 'function';\n};\n","var fails = require('../internals/fails');\nvar isCallable = require('../internals/is-callable');\n\nvar replacement = /#|\\.prototype\\./;\n\nvar isForced = function (feature, detection) {\n var value = data[normalize(feature)];\n return value == POLYFILL ? true\n : value == NATIVE ? false\n : isCallable(detection) ? fails(detection)\n : !!detection;\n};\n\nvar normalize = isForced.normalize = function (string) {\n return String(string).replace(replacement, '.').toLowerCase();\n};\n\nvar data = isForced.data = {};\nvar NATIVE = isForced.NATIVE = 'N';\nvar POLYFILL = isForced.POLYFILL = 'P';\n\nmodule.exports = isForced;\n","// we can't use just `it == null` since of `document.all` special case\n// https://tc39.es/ecma262/#sec-IsHTMLDDA-internal-slot-aec\nmodule.exports = function (it) {\n return it === null || it === undefined;\n};\n","var isCallable = require('../internals/is-callable');\nvar $documentAll = require('../internals/document-all');\n\nvar documentAll = $documentAll.all;\n\nmodule.exports = $documentAll.IS_HTMLDDA ? function (it) {\n return typeof it == 'object' ? it !== null : isCallable(it) || it === documentAll;\n} : function (it) {\n return typeof it == 'object' ? it !== null : isCallable(it);\n};\n","module.exports = true;\n","var getBuiltIn = require('../internals/get-built-in');\nvar isCallable = require('../internals/is-callable');\nvar isPrototypeOf = require('../internals/object-is-prototype-of');\nvar USE_SYMBOL_AS_UID = require('../internals/use-symbol-as-uid');\n\nvar $Object = Object;\n\nmodule.exports = USE_SYMBOL_AS_UID ? function (it) {\n return typeof it == 'symbol';\n} : function (it) {\n var $Symbol = getBuiltIn('Symbol');\n return isCallable($Symbol) && isPrototypeOf($Symbol.prototype, $Object(it));\n};\n","var DESCRIPTORS = require('../internals/descriptors');\nvar IE8_DOM_DEFINE = require('../internals/ie8-dom-define');\nvar V8_PROTOTYPE_DEFINE_BUG = require('../internals/v8-prototype-define-bug');\nvar anObject = require('../internals/an-object');\nvar toPropertyKey = require('../internals/to-property-key');\n\nvar $TypeError = TypeError;\n// eslint-disable-next-line es/no-object-defineproperty -- safe\nvar $defineProperty = Object.defineProperty;\n// eslint-disable-next-line es/no-object-getownpropertydescriptor -- safe\nvar $getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;\nvar ENUMERABLE = 'enumerable';\nvar CONFIGURABLE = 'configurable';\nvar WRITABLE = 'writable';\n\n// `Object.defineProperty` method\n// https://tc39.es/ecma262/#sec-object.defineproperty\nexports.f = DESCRIPTORS ? V8_PROTOTYPE_DEFINE_BUG ? function defineProperty(O, P, Attributes) {\n anObject(O);\n P = toPropertyKey(P);\n anObject(Attributes);\n if (typeof O === 'function' && P === 'prototype' && 'value' in Attributes && WRITABLE in Attributes && !Attributes[WRITABLE]) {\n var current = $getOwnPropertyDescriptor(O, P);\n if (current && current[WRITABLE]) {\n O[P] = Attributes.value;\n Attributes = {\n configurable: CONFIGURABLE in Attributes ? Attributes[CONFIGURABLE] : current[CONFIGURABLE],\n enumerable: ENUMERABLE in Attributes ? Attributes[ENUMERABLE] : current[ENUMERABLE],\n writable: false\n };\n }\n } return $defineProperty(O, P, Attributes);\n} : $defineProperty : function defineProperty(O, P, Attributes) {\n anObject(O);\n P = toPropertyKey(P);\n anObject(Attributes);\n if (IE8_DOM_DEFINE) try {\n return $defineProperty(O, P, Attributes);\n } catch (error) { /* empty */ }\n if ('get' in Attributes || 'set' in Attributes) throw $TypeError('Accessors not supported');\n if ('value' in Attributes) O[P] = Attributes.value;\n return O;\n};\n","var DESCRIPTORS = require('../internals/descriptors');\nvar call = require('../internals/function-call');\nvar propertyIsEnumerableModule = require('../internals/object-property-is-enumerable');\nvar createPropertyDescriptor = require('../internals/create-property-descriptor');\nvar toIndexedObject = require('../internals/to-indexed-object');\nvar toPropertyKey = require('../internals/to-property-key');\nvar hasOwn = require('../internals/has-own-property');\nvar IE8_DOM_DEFINE = require('../internals/ie8-dom-define');\n\n// eslint-disable-next-line es/no-object-getownpropertydescriptor -- safe\nvar $getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;\n\n// `Object.getOwnPropertyDescriptor` method\n// https://tc39.es/ecma262/#sec-object.getownpropertydescriptor\nexports.f = DESCRIPTORS ? $getOwnPropertyDescriptor : function getOwnPropertyDescriptor(O, P) {\n O = toIndexedObject(O);\n P = toPropertyKey(P);\n if (IE8_DOM_DEFINE) try {\n return $getOwnPropertyDescriptor(O, P);\n } catch (error) { /* empty */ }\n if (hasOwn(O, P)) return createPropertyDescriptor(!call(propertyIsEnumerableModule.f, O, P), O[P]);\n};\n","var uncurryThis = require('../internals/function-uncurry-this');\n\nmodule.exports = uncurryThis({}.isPrototypeOf);\n","'use strict';\nvar $propertyIsEnumerable = {}.propertyIsEnumerable;\n// eslint-disable-next-line es/no-object-getownpropertydescriptor -- safe\nvar getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;\n\n// Nashorn ~ JDK8 bug\nvar NASHORN_BUG = getOwnPropertyDescriptor && !$propertyIsEnumerable.call({ 1: 2 }, 1);\n\n// `Object.prototype.propertyIsEnumerable` method implementation\n// https://tc39.es/ecma262/#sec-object.prototype.propertyisenumerable\nexports.f = NASHORN_BUG ? function propertyIsEnumerable(V) {\n var descriptor = getOwnPropertyDescriptor(this, V);\n return !!descriptor && descriptor.enumerable;\n} : $propertyIsEnumerable;\n","var call = require('../internals/function-call');\nvar isCallable = require('../internals/is-callable');\nvar isObject = require('../internals/is-object');\n\nvar $TypeError = TypeError;\n\n// `OrdinaryToPrimitive` abstract operation\n// https://tc39.es/ecma262/#sec-ordinarytoprimitive\nmodule.exports = function (input, pref) {\n var fn, val;\n if (pref === 'string' && isCallable(fn = input.toString) && !isObject(val = call(fn, input))) return val;\n if (isCallable(fn = input.valueOf) && !isObject(val = call(fn, input))) return val;\n if (pref !== 'string' && isCallable(fn = input.toString) && !isObject(val = call(fn, input))) return val;\n throw $TypeError(\"Can't convert object to primitive value\");\n};\n","module.exports = {};\n","var isNullOrUndefined = require('../internals/is-null-or-undefined');\n\nvar $TypeError = TypeError;\n\n// `RequireObjectCoercible` abstract operation\n// https://tc39.es/ecma262/#sec-requireobjectcoercible\nmodule.exports = function (it) {\n if (isNullOrUndefined(it)) throw $TypeError(\"Can't call method on \" + it);\n return it;\n};\n","var global = require('../internals/global');\nvar defineGlobalProperty = require('../internals/define-global-property');\n\nvar SHARED = '__core-js_shared__';\nvar store = global[SHARED] || defineGlobalProperty(SHARED, {});\n\nmodule.exports = store;\n","var IS_PURE = require('../internals/is-pure');\nvar store = require('../internals/shared-store');\n\n(module.exports = function (key, value) {\n return store[key] || (store[key] = value !== undefined ? value : {});\n})('versions', []).push({\n version: '3.31.0',\n mode: IS_PURE ? 'pure' : 'global',\n copyright: '© 2014-2023 Denis Pushkarev (zloirock.ru)',\n license: 'https://github.com/zloirock/core-js/blob/v3.31.0/LICENSE',\n source: 'https://github.com/zloirock/core-js'\n});\n","/* eslint-disable es/no-symbol -- required for testing */\nvar V8_VERSION = require('../internals/engine-v8-version');\nvar fails = require('../internals/fails');\nvar global = require('../internals/global');\n\nvar $String = global.String;\n\n// eslint-disable-next-line es/no-object-getownpropertysymbols -- required for testing\nmodule.exports = !!Object.getOwnPropertySymbols && !fails(function () {\n var symbol = Symbol();\n // Chrome 38 Symbol has incorrect toString conversion\n // `get-own-property-symbols` polyfill symbols converted to object are not Symbol instances\n // nb: Do not call `String` directly to avoid this being optimized out to `symbol+''` which will,\n // of course, fail.\n return !$String(symbol) || !(Object(symbol) instanceof Symbol) ||\n // Chrome 38-40 symbols are not inherited from DOM collections prototypes to instances\n !Symbol.sham && V8_VERSION && V8_VERSION < 41;\n});\n","// toObject with fallback for non-array-like ES3 strings\nvar IndexedObject = require('../internals/indexed-object');\nvar requireObjectCoercible = require('../internals/require-object-coercible');\n\nmodule.exports = function (it) {\n return IndexedObject(requireObjectCoercible(it));\n};\n","var requireObjectCoercible = require('../internals/require-object-coercible');\n\nvar $Object = Object;\n\n// `ToObject` abstract operation\n// https://tc39.es/ecma262/#sec-toobject\nmodule.exports = function (argument) {\n return $Object(requireObjectCoercible(argument));\n};\n","var call = require('../internals/function-call');\nvar isObject = require('../internals/is-object');\nvar isSymbol = require('../internals/is-symbol');\nvar getMethod = require('../internals/get-method');\nvar ordinaryToPrimitive = require('../internals/ordinary-to-primitive');\nvar wellKnownSymbol = require('../internals/well-known-symbol');\n\nvar $TypeError = TypeError;\nvar TO_PRIMITIVE = wellKnownSymbol('toPrimitive');\n\n// `ToPrimitive` abstract operation\n// https://tc39.es/ecma262/#sec-toprimitive\nmodule.exports = function (input, pref) {\n if (!isObject(input) || isSymbol(input)) return input;\n var exoticToPrim = getMethod(input, TO_PRIMITIVE);\n var result;\n if (exoticToPrim) {\n if (pref === undefined) pref = 'default';\n result = call(exoticToPrim, input, pref);\n if (!isObject(result) || isSymbol(result)) return result;\n throw $TypeError(\"Can't convert object to primitive value\");\n }\n if (pref === undefined) pref = 'number';\n return ordinaryToPrimitive(input, pref);\n};\n","var toPrimitive = require('../internals/to-primitive');\nvar isSymbol = require('../internals/is-symbol');\n\n// `ToPropertyKey` abstract operation\n// https://tc39.es/ecma262/#sec-topropertykey\nmodule.exports = function (argument) {\n var key = toPrimitive(argument, 'string');\n return isSymbol(key) ? key : key + '';\n};\n","var $String = String;\n\nmodule.exports = function (argument) {\n try {\n return $String(argument);\n } catch (error) {\n return 'Object';\n }\n};\n","var uncurryThis = require('../internals/function-uncurry-this');\n\nvar id = 0;\nvar postfix = Math.random();\nvar toString = uncurryThis(1.0.toString);\n\nmodule.exports = function (key) {\n return 'Symbol(' + (key === undefined ? '' : key) + ')_' + toString(++id + postfix, 36);\n};\n","/* eslint-disable es/no-symbol -- required for testing */\nvar NATIVE_SYMBOL = require('../internals/symbol-constructor-detection');\n\nmodule.exports = NATIVE_SYMBOL\n && !Symbol.sham\n && typeof Symbol.iterator == 'symbol';\n","var DESCRIPTORS = require('../internals/descriptors');\nvar fails = require('../internals/fails');\n\n// V8 ~ Chrome 36-\n// https://bugs.chromium.org/p/v8/issues/detail?id=3334\nmodule.exports = DESCRIPTORS && fails(function () {\n // eslint-disable-next-line es/no-object-defineproperty -- required for testing\n return Object.defineProperty(function () { /* empty */ }, 'prototype', {\n value: 42,\n writable: false\n }).prototype != 42;\n});\n","var global = require('../internals/global');\nvar shared = require('../internals/shared');\nvar hasOwn = require('../internals/has-own-property');\nvar uid = require('../internals/uid');\nvar NATIVE_SYMBOL = require('../internals/symbol-constructor-detection');\nvar USE_SYMBOL_AS_UID = require('../internals/use-symbol-as-uid');\n\nvar Symbol = global.Symbol;\nvar WellKnownSymbolsStore = shared('wks');\nvar createWellKnownSymbol = USE_SYMBOL_AS_UID ? Symbol['for'] || Symbol : Symbol && Symbol.withoutSetter || uid;\n\nmodule.exports = function (name) {\n if (!hasOwn(WellKnownSymbolsStore, name)) {\n WellKnownSymbolsStore[name] = NATIVE_SYMBOL && hasOwn(Symbol, name)\n ? Symbol[name]\n : createWellKnownSymbol('Symbol.' + name);\n } return WellKnownSymbolsStore[name];\n};\n","var $ = require('../internals/export');\nvar DESCRIPTORS = require('../internals/descriptors');\nvar defineProperty = require('../internals/object-define-property').f;\n\n// `Object.defineProperty` method\n// https://tc39.es/ecma262/#sec-object.defineproperty\n// eslint-disable-next-line es/no-object-defineproperty -- safe\n$({ target: 'Object', stat: true, forced: Object.defineProperty !== defineProperty, sham: !DESCRIPTORS }, {\n defineProperty: defineProperty\n});\n","var parent = require('../../es/object/define-property');\n\nmodule.exports = parent;\n","/*!\n * @description Recursive object extending\n * @author Viacheslav Lotsmanov \n * @license MIT\n *\n * The MIT License (MIT)\n *\n * Copyright (c) 2013-2018 Viacheslav Lotsmanov\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n'use strict';\n\nfunction isSpecificValue(val) {\n\treturn (\n\t\tval instanceof Buffer\n\t\t|| val instanceof Date\n\t\t|| val instanceof RegExp\n\t) ? true : false;\n}\n\nfunction cloneSpecificValue(val) {\n\tif (val instanceof Buffer) {\n\t\tvar x = Buffer.alloc\n\t\t\t? Buffer.alloc(val.length)\n\t\t\t: new Buffer(val.length);\n\t\tval.copy(x);\n\t\treturn x;\n\t} else if (val instanceof Date) {\n\t\treturn new Date(val.getTime());\n\t} else if (val instanceof RegExp) {\n\t\treturn new RegExp(val);\n\t} else {\n\t\tthrow new Error('Unexpected situation');\n\t}\n}\n\n/**\n * Recursive cloning array.\n */\nfunction deepCloneArray(arr) {\n\tvar clone = [];\n\tarr.forEach(function (item, index) {\n\t\tif (typeof item === 'object' && item !== null) {\n\t\t\tif (Array.isArray(item)) {\n\t\t\t\tclone[index] = deepCloneArray(item);\n\t\t\t} else if (isSpecificValue(item)) {\n\t\t\t\tclone[index] = cloneSpecificValue(item);\n\t\t\t} else {\n\t\t\t\tclone[index] = deepExtend({}, item);\n\t\t\t}\n\t\t} else {\n\t\t\tclone[index] = item;\n\t\t}\n\t});\n\treturn clone;\n}\n\nfunction safeGetProperty(object, property) {\n\treturn property === '__proto__' ? undefined : object[property];\n}\n\n/**\n * Extening object that entered in first argument.\n *\n * Returns extended object or false if have no target object or incorrect type.\n *\n * If you wish to clone source object (without modify it), just use empty new\n * object as first argument, like this:\n * deepExtend({}, yourObj_1, [yourObj_N]);\n */\nvar deepExtend = module.exports = function (/*obj_1, [obj_2], [obj_N]*/) {\n\tif (arguments.length < 1 || typeof arguments[0] !== 'object') {\n\t\treturn false;\n\t}\n\n\tif (arguments.length < 2) {\n\t\treturn arguments[0];\n\t}\n\n\tvar target = arguments[0];\n\n\t// convert arguments to array and cut off target object\n\tvar args = Array.prototype.slice.call(arguments, 1);\n\n\tvar val, src, clone;\n\n\targs.forEach(function (obj) {\n\t\t// skip argument if isn't an object, is null, or is an array\n\t\tif (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {\n\t\t\treturn;\n\t\t}\n\n\t\tObject.keys(obj).forEach(function (key) {\n\t\t\tsrc = safeGetProperty(target, key); // source value\n\t\t\tval = safeGetProperty(obj, key); // new value\n\n\t\t\t// recursion prevention\n\t\t\tif (val === target) {\n\t\t\t\treturn;\n\n\t\t\t/**\n\t\t\t * if new value isn't object then just overwrite by new value\n\t\t\t * instead of extending.\n\t\t\t */\n\t\t\t} else if (typeof val !== 'object' || val === null) {\n\t\t\t\ttarget[key] = val;\n\t\t\t\treturn;\n\n\t\t\t// just clone arrays (and recursive clone objects inside)\n\t\t\t} else if (Array.isArray(val)) {\n\t\t\t\ttarget[key] = deepCloneArray(val);\n\t\t\t\treturn;\n\n\t\t\t// custom cloning and overwrite for specific objects\n\t\t\t} else if (isSpecificValue(val)) {\n\t\t\t\ttarget[key] = cloneSpecificValue(val);\n\t\t\t\treturn;\n\n\t\t\t// overwrite by new value if source isn't object or array\n\t\t\t} else if (typeof src !== 'object' || src === null || Array.isArray(src)) {\n\t\t\t\ttarget[key] = deepExtend({}, val);\n\t\t\t\treturn;\n\n\t\t\t// source value and new value is objects both, extending...\n\t\t\t} else {\n\t\t\t\ttarget[key] = deepExtend(src, val);\n\t\t\t\treturn;\n\t\t\t}\n\t\t});\n\t});\n\n\treturn target;\n};\n","// Copyright Joyent, Inc. and other Node contributors.\n//\n// Permission is hereby granted, free of charge, to any person obtaining a\n// copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to permit\n// persons to whom the Software is furnished to do so, subject to the\n// following conditions:\n//\n// The above copyright notice and this permission notice shall be included\n// in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\n// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN\n// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\n// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE\n// USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n'use strict';\n\nvar R = typeof Reflect === 'object' ? Reflect : null\nvar ReflectApply = R && typeof R.apply === 'function'\n ? R.apply\n : function ReflectApply(target, receiver, args) {\n return Function.prototype.apply.call(target, receiver, args);\n }\n\nvar ReflectOwnKeys\nif (R && typeof R.ownKeys === 'function') {\n ReflectOwnKeys = R.ownKeys\n} else if (Object.getOwnPropertySymbols) {\n ReflectOwnKeys = function ReflectOwnKeys(target) {\n return Object.getOwnPropertyNames(target)\n .concat(Object.getOwnPropertySymbols(target));\n };\n} else {\n ReflectOwnKeys = function ReflectOwnKeys(target) {\n return Object.getOwnPropertyNames(target);\n };\n}\n\nfunction ProcessEmitWarning(warning) {\n if (console && console.warn) console.warn(warning);\n}\n\nvar NumberIsNaN = Number.isNaN || function NumberIsNaN(value) {\n return value !== value;\n}\n\nfunction EventEmitter() {\n EventEmitter.init.call(this);\n}\nmodule.exports = EventEmitter;\nmodule.exports.once = once;\n\n// Backwards-compat with node 0.10.x\nEventEmitter.EventEmitter = EventEmitter;\n\nEventEmitter.prototype._events = undefined;\nEventEmitter.prototype._eventsCount = 0;\nEventEmitter.prototype._maxListeners = undefined;\n\n// By default EventEmitters will print a warning if more than 10 listeners are\n// added to it. This is a useful default which helps finding memory leaks.\nvar defaultMaxListeners = 10;\n\nfunction checkListener(listener) {\n if (typeof listener !== 'function') {\n throw new TypeError('The \"listener\" argument must be of type Function. Received type ' + typeof listener);\n }\n}\n\nObject.defineProperty(EventEmitter, 'defaultMaxListeners', {\n enumerable: true,\n get: function() {\n return defaultMaxListeners;\n },\n set: function(arg) {\n if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) {\n throw new RangeError('The value of \"defaultMaxListeners\" is out of range. It must be a non-negative number. Received ' + arg + '.');\n }\n defaultMaxListeners = arg;\n }\n});\n\nEventEmitter.init = function() {\n\n if (this._events === undefined ||\n this._events === Object.getPrototypeOf(this)._events) {\n this._events = Object.create(null);\n this._eventsCount = 0;\n }\n\n this._maxListeners = this._maxListeners || undefined;\n};\n\n// Obviously not all Emitters should be limited to 10. This function allows\n// that to be increased. Set to zero for unlimited.\nEventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {\n if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) {\n throw new RangeError('The value of \"n\" is out of range. It must be a non-negative number. Received ' + n + '.');\n }\n this._maxListeners = n;\n return this;\n};\n\nfunction _getMaxListeners(that) {\n if (that._maxListeners === undefined)\n return EventEmitter.defaultMaxListeners;\n return that._maxListeners;\n}\n\nEventEmitter.prototype.getMaxListeners = function getMaxListeners() {\n return _getMaxListeners(this);\n};\n\nEventEmitter.prototype.emit = function emit(type) {\n var args = [];\n for (var i = 1; i < arguments.length; i++) args.push(arguments[i]);\n var doError = (type === 'error');\n\n var events = this._events;\n if (events !== undefined)\n doError = (doError && events.error === undefined);\n else if (!doError)\n return false;\n\n // If there is no 'error' event listener then throw.\n if (doError) {\n var er;\n if (args.length > 0)\n er = args[0];\n if (er instanceof Error) {\n // Note: The comments on the `throw` lines are intentional, they show\n // up in Node's output if this results in an unhandled exception.\n throw er; // Unhandled 'error' event\n }\n // At least give some kind of context to the user\n var err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : ''));\n err.context = er;\n throw err; // Unhandled 'error' event\n }\n\n var handler = events[type];\n\n if (handler === undefined)\n return false;\n\n if (typeof handler === 'function') {\n ReflectApply(handler, this, args);\n } else {\n var len = handler.length;\n var listeners = arrayClone(handler, len);\n for (var i = 0; i < len; ++i)\n ReflectApply(listeners[i], this, args);\n }\n\n return true;\n};\n\nfunction _addListener(target, type, listener, prepend) {\n var m;\n var events;\n var existing;\n\n checkListener(listener);\n\n events = target._events;\n if (events === undefined) {\n events = target._events = Object.create(null);\n target._eventsCount = 0;\n } else {\n // To avoid recursion in the case that type === \"newListener\"! Before\n // adding it to the listeners, first emit \"newListener\".\n if (events.newListener !== undefined) {\n target.emit('newListener', type,\n listener.listener ? listener.listener : listener);\n\n // Re-assign `events` because a newListener handler could have caused the\n // this._events to be assigned to a new object\n events = target._events;\n }\n existing = events[type];\n }\n\n if (existing === undefined) {\n // Optimize the case of one listener. Don't need the extra array object.\n existing = events[type] = listener;\n ++target._eventsCount;\n } else {\n if (typeof existing === 'function') {\n // Adding the second element, need to change to array.\n existing = events[type] =\n prepend ? [listener, existing] : [existing, listener];\n // If we've already got an array, just append.\n } else if (prepend) {\n existing.unshift(listener);\n } else {\n existing.push(listener);\n }\n\n // Check for listener leak\n m = _getMaxListeners(target);\n if (m > 0 && existing.length > m && !existing.warned) {\n existing.warned = true;\n // No error code for this since it is a Warning\n // eslint-disable-next-line no-restricted-syntax\n var w = new Error('Possible EventEmitter memory leak detected. ' +\n existing.length + ' ' + String(type) + ' listeners ' +\n 'added. Use emitter.setMaxListeners() to ' +\n 'increase limit');\n w.name = 'MaxListenersExceededWarning';\n w.emitter = target;\n w.type = type;\n w.count = existing.length;\n ProcessEmitWarning(w);\n }\n }\n\n return target;\n}\n\nEventEmitter.prototype.addListener = function addListener(type, listener) {\n return _addListener(this, type, listener, false);\n};\n\nEventEmitter.prototype.on = EventEmitter.prototype.addListener;\n\nEventEmitter.prototype.prependListener =\n function prependListener(type, listener) {\n return _addListener(this, type, listener, true);\n };\n\nfunction onceWrapper() {\n if (!this.fired) {\n this.target.removeListener(this.type, this.wrapFn);\n this.fired = true;\n if (arguments.length === 0)\n return this.listener.call(this.target);\n return this.listener.apply(this.target, arguments);\n }\n}\n\nfunction _onceWrap(target, type, listener) {\n var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener };\n var wrapped = onceWrapper.bind(state);\n wrapped.listener = listener;\n state.wrapFn = wrapped;\n return wrapped;\n}\n\nEventEmitter.prototype.once = function once(type, listener) {\n checkListener(listener);\n this.on(type, _onceWrap(this, type, listener));\n return this;\n};\n\nEventEmitter.prototype.prependOnceListener =\n function prependOnceListener(type, listener) {\n checkListener(listener);\n this.prependListener(type, _onceWrap(this, type, listener));\n return this;\n };\n\n// Emits a 'removeListener' event if and only if the listener was removed.\nEventEmitter.prototype.removeListener =\n function removeListener(type, listener) {\n var list, events, position, i, originalListener;\n\n checkListener(listener);\n\n events = this._events;\n if (events === undefined)\n return this;\n\n list = events[type];\n if (list === undefined)\n return this;\n\n if (list === listener || list.listener === listener) {\n if (--this._eventsCount === 0)\n this._events = Object.create(null);\n else {\n delete events[type];\n if (events.removeListener)\n this.emit('removeListener', type, list.listener || listener);\n }\n } else if (typeof list !== 'function') {\n position = -1;\n\n for (i = list.length - 1; i >= 0; i--) {\n if (list[i] === listener || list[i].listener === listener) {\n originalListener = list[i].listener;\n position = i;\n break;\n }\n }\n\n if (position < 0)\n return this;\n\n if (position === 0)\n list.shift();\n else {\n spliceOne(list, position);\n }\n\n if (list.length === 1)\n events[type] = list[0];\n\n if (events.removeListener !== undefined)\n this.emit('removeListener', type, originalListener || listener);\n }\n\n return this;\n };\n\nEventEmitter.prototype.off = EventEmitter.prototype.removeListener;\n\nEventEmitter.prototype.removeAllListeners =\n function removeAllListeners(type) {\n var listeners, events, i;\n\n events = this._events;\n if (events === undefined)\n return this;\n\n // not listening for removeListener, no need to emit\n if (events.removeListener === undefined) {\n if (arguments.length === 0) {\n this._events = Object.create(null);\n this._eventsCount = 0;\n } else if (events[type] !== undefined) {\n if (--this._eventsCount === 0)\n this._events = Object.create(null);\n else\n delete events[type];\n }\n return this;\n }\n\n // emit removeListener for all listeners on all events\n if (arguments.length === 0) {\n var keys = Object.keys(events);\n var key;\n for (i = 0; i < keys.length; ++i) {\n key = keys[i];\n if (key === 'removeListener') continue;\n this.removeAllListeners(key);\n }\n this.removeAllListeners('removeListener');\n this._events = Object.create(null);\n this._eventsCount = 0;\n return this;\n }\n\n listeners = events[type];\n\n if (typeof listeners === 'function') {\n this.removeListener(type, listeners);\n } else if (listeners !== undefined) {\n // LIFO order\n for (i = listeners.length - 1; i >= 0; i--) {\n this.removeListener(type, listeners[i]);\n }\n }\n\n return this;\n };\n\nfunction _listeners(target, type, unwrap) {\n var events = target._events;\n\n if (events === undefined)\n return [];\n\n var evlistener = events[type];\n if (evlistener === undefined)\n return [];\n\n if (typeof evlistener === 'function')\n return unwrap ? [evlistener.listener || evlistener] : [evlistener];\n\n return unwrap ?\n unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length);\n}\n\nEventEmitter.prototype.listeners = function listeners(type) {\n return _listeners(this, type, true);\n};\n\nEventEmitter.prototype.rawListeners = function rawListeners(type) {\n return _listeners(this, type, false);\n};\n\nEventEmitter.listenerCount = function(emitter, type) {\n if (typeof emitter.listenerCount === 'function') {\n return emitter.listenerCount(type);\n } else {\n return listenerCount.call(emitter, type);\n }\n};\n\nEventEmitter.prototype.listenerCount = listenerCount;\nfunction listenerCount(type) {\n var events = this._events;\n\n if (events !== undefined) {\n var evlistener = events[type];\n\n if (typeof evlistener === 'function') {\n return 1;\n } else if (evlistener !== undefined) {\n return evlistener.length;\n }\n }\n\n return 0;\n}\n\nEventEmitter.prototype.eventNames = function eventNames() {\n return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : [];\n};\n\nfunction arrayClone(arr, n) {\n var copy = new Array(n);\n for (var i = 0; i < n; ++i)\n copy[i] = arr[i];\n return copy;\n}\n\nfunction spliceOne(list, index) {\n for (; index + 1 < list.length; index++)\n list[index] = list[index + 1];\n list.pop();\n}\n\nfunction unwrapListeners(arr) {\n var ret = new Array(arr.length);\n for (var i = 0; i < ret.length; ++i) {\n ret[i] = arr[i].listener || arr[i];\n }\n return ret;\n}\n\nfunction once(emitter, name) {\n return new Promise(function (resolve, reject) {\n function errorListener(err) {\n emitter.removeListener(name, resolver);\n reject(err);\n }\n\n function resolver() {\n if (typeof emitter.removeListener === 'function') {\n emitter.removeListener('error', errorListener);\n }\n resolve([].slice.call(arguments));\n };\n\n eventTargetAgnosticAddListener(emitter, name, resolver, { once: true });\n if (name !== 'error') {\n addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true });\n }\n });\n}\n\nfunction addErrorHandlerIfEventEmitter(emitter, handler, flags) {\n if (typeof emitter.on === 'function') {\n eventTargetAgnosticAddListener(emitter, 'error', handler, flags);\n }\n}\n\nfunction eventTargetAgnosticAddListener(emitter, name, listener, flags) {\n if (typeof emitter.on === 'function') {\n if (flags.once) {\n emitter.once(name, listener);\n } else {\n emitter.on(name, listener);\n }\n } else if (typeof emitter.addEventListener === 'function') {\n // EventTarget does not have `error` event semantics like Node\n // EventEmitters, we do not listen for `error` events here.\n emitter.addEventListener(name, function wrapListener(arg) {\n // IE does not have builtin `{ once: true }` support so we\n // have to do it manually.\n if (flags.once) {\n emitter.removeEventListener(name, wrapListener);\n }\n listener(arg);\n });\n } else {\n throw new TypeError('The \"emitter\" argument must be of type EventEmitter. Received type ' + typeof emitter);\n }\n}\n","if (typeof Object.create === 'function') {\n // implementation from standard node.js 'util' module\n module.exports = function inherits(ctor, superCtor) {\n if (superCtor) {\n ctor.super_ = superCtor\n ctor.prototype = Object.create(superCtor.prototype, {\n constructor: {\n value: ctor,\n enumerable: false,\n writable: true,\n configurable: true\n }\n })\n }\n };\n} else {\n // old school shim for old browsers\n module.exports = function inherits(ctor, superCtor) {\n if (superCtor) {\n ctor.super_ = superCtor\n var TempCtor = function () {}\n TempCtor.prototype = superCtor.prototype\n ctor.prototype = new TempCtor()\n ctor.prototype.constructor = ctor\n }\n }\n}\n","// shim for using process in browser\nvar process = module.exports = {};\n\n// cached from whatever global is present so that test runners that stub it\n// don't break things. But we need to wrap it in a try catch in case it is\n// wrapped in strict mode code which doesn't define any globals. It's inside a\n// function because try/catches deoptimize in certain engines.\n\nvar cachedSetTimeout;\nvar cachedClearTimeout;\n\nfunction defaultSetTimout() {\n throw new Error('setTimeout has not been defined');\n}\nfunction defaultClearTimeout () {\n throw new Error('clearTimeout has not been defined');\n}\n(function () {\n try {\n if (typeof setTimeout === 'function') {\n cachedSetTimeout = setTimeout;\n } else {\n cachedSetTimeout = defaultSetTimout;\n }\n } catch (e) {\n cachedSetTimeout = defaultSetTimout;\n }\n try {\n if (typeof clearTimeout === 'function') {\n cachedClearTimeout = clearTimeout;\n } else {\n cachedClearTimeout = defaultClearTimeout;\n }\n } catch (e) {\n cachedClearTimeout = defaultClearTimeout;\n }\n} ())\nfunction runTimeout(fun) {\n if (cachedSetTimeout === setTimeout) {\n //normal enviroments in sane situations\n return setTimeout(fun, 0);\n }\n // if setTimeout wasn't available but was latter defined\n if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) {\n cachedSetTimeout = setTimeout;\n return setTimeout(fun, 0);\n }\n try {\n // when when somebody has screwed with setTimeout but no I.E. maddness\n return cachedSetTimeout(fun, 0);\n } catch(e){\n try {\n // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally\n return cachedSetTimeout.call(null, fun, 0);\n } catch(e){\n // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error\n return cachedSetTimeout.call(this, fun, 0);\n }\n }\n\n\n}\nfunction runClearTimeout(marker) {\n if (cachedClearTimeout === clearTimeout) {\n //normal enviroments in sane situations\n return clearTimeout(marker);\n }\n // if clearTimeout wasn't available but was latter defined\n if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) {\n cachedClearTimeout = clearTimeout;\n return clearTimeout(marker);\n }\n try {\n // when when somebody has screwed with setTimeout but no I.E. maddness\n return cachedClearTimeout(marker);\n } catch (e){\n try {\n // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally\n return cachedClearTimeout.call(null, marker);\n } catch (e){\n // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error.\n // Some versions of I.E. have different rules for clearTimeout vs setTimeout\n return cachedClearTimeout.call(this, marker);\n }\n }\n\n\n\n}\nvar queue = [];\nvar draining = false;\nvar currentQueue;\nvar queueIndex = -1;\n\nfunction cleanUpNextTick() {\n if (!draining || !currentQueue) {\n return;\n }\n draining = false;\n if (currentQueue.length) {\n queue = currentQueue.concat(queue);\n } else {\n queueIndex = -1;\n }\n if (queue.length) {\n drainQueue();\n }\n}\n\nfunction drainQueue() {\n if (draining) {\n return;\n }\n var timeout = runTimeout(cleanUpNextTick);\n draining = true;\n\n var len = queue.length;\n while(len) {\n currentQueue = queue;\n queue = [];\n while (++queueIndex < len) {\n if (currentQueue) {\n currentQueue[queueIndex].run();\n }\n }\n queueIndex = -1;\n len = queue.length;\n }\n currentQueue = null;\n draining = false;\n runClearTimeout(timeout);\n}\n\nprocess.nextTick = function (fun) {\n var args = new Array(arguments.length - 1);\n if (arguments.length > 1) {\n for (var i = 1; i < arguments.length; i++) {\n args[i - 1] = arguments[i];\n }\n }\n queue.push(new Item(fun, args));\n if (queue.length === 1 && !draining) {\n runTimeout(drainQueue);\n }\n};\n\n// v8 likes predictible objects\nfunction Item(fun, array) {\n this.fun = fun;\n this.array = array;\n}\nItem.prototype.run = function () {\n this.fun.apply(null, this.array);\n};\nprocess.title = 'browser';\nprocess.browser = true;\nprocess.env = {};\nprocess.argv = [];\nprocess.version = ''; // empty string to avoid regexp issues\nprocess.versions = {};\n\nfunction noop() {}\n\nprocess.on = noop;\nprocess.addListener = noop;\nprocess.once = noop;\nprocess.off = noop;\nprocess.removeListener = noop;\nprocess.removeAllListeners = noop;\nprocess.emit = noop;\nprocess.prependListener = noop;\nprocess.prependOnceListener = noop;\n\nprocess.listeners = function (name) { return [] }\n\nprocess.binding = function (name) {\n throw new Error('process.binding is not supported');\n};\n\nprocess.cwd = function () { return '/' };\nprocess.chdir = function (dir) {\n throw new Error('process.chdir is not supported');\n};\nprocess.umask = function() { return 0; };\n","'use strict'\n\n// limit of Crypto.getRandomValues()\n// https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues\nvar MAX_BYTES = 65536\n\n// Node supports requesting up to this number of bytes\n// https://github.com/nodejs/node/blob/master/lib/internal/crypto/random.js#L48\nvar MAX_UINT32 = 4294967295\n\nfunction oldBrowser () {\n throw new Error('Secure random number generation is not supported by this browser.\\nUse Chrome, Firefox or Internet Explorer 11')\n}\n\nvar Buffer = require('safe-buffer').Buffer\nvar crypto = global.crypto || global.msCrypto\n\nif (crypto && crypto.getRandomValues) {\n module.exports = randomBytes\n} else {\n module.exports = oldBrowser\n}\n\nfunction randomBytes (size, cb) {\n // phantomjs needs to throw\n if (size > MAX_UINT32) throw new RangeError('requested too many random bytes')\n\n var bytes = Buffer.allocUnsafe(size)\n\n if (size > 0) { // getRandomValues fails on IE if size == 0\n if (size > MAX_BYTES) { // this is the max bytes crypto.getRandomValues\n // can do at once see https://developer.mozilla.org/en-US/docs/Web/API/window.crypto.getRandomValues\n for (var generated = 0; generated < size; generated += MAX_BYTES) {\n // buffer.slice automatically checks if the end is past the end of\n // the buffer so we don't have to here\n crypto.getRandomValues(bytes.slice(generated, generated + MAX_BYTES))\n }\n } else {\n crypto.getRandomValues(bytes)\n }\n }\n\n if (typeof cb === 'function') {\n return process.nextTick(function () {\n cb(null, bytes)\n })\n }\n\n return bytes\n}\n","'use strict';\n\nfunction _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; }\n\nvar codes = {};\n\nfunction createErrorType(code, message, Base) {\n if (!Base) {\n Base = Error;\n }\n\n function getMessage(arg1, arg2, arg3) {\n if (typeof message === 'string') {\n return message;\n } else {\n return message(arg1, arg2, arg3);\n }\n }\n\n var NodeError =\n /*#__PURE__*/\n function (_Base) {\n _inheritsLoose(NodeError, _Base);\n\n function NodeError(arg1, arg2, arg3) {\n return _Base.call(this, getMessage(arg1, arg2, arg3)) || this;\n }\n\n return NodeError;\n }(Base);\n\n NodeError.prototype.name = Base.name;\n NodeError.prototype.code = code;\n codes[code] = NodeError;\n} // https://github.com/nodejs/node/blob/v10.8.0/lib/internal/errors.js\n\n\nfunction oneOf(expected, thing) {\n if (Array.isArray(expected)) {\n var len = expected.length;\n expected = expected.map(function (i) {\n return String(i);\n });\n\n if (len > 2) {\n return \"one of \".concat(thing, \" \").concat(expected.slice(0, len - 1).join(', '), \", or \") + expected[len - 1];\n } else if (len === 2) {\n return \"one of \".concat(thing, \" \").concat(expected[0], \" or \").concat(expected[1]);\n } else {\n return \"of \".concat(thing, \" \").concat(expected[0]);\n }\n } else {\n return \"of \".concat(thing, \" \").concat(String(expected));\n }\n} // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith\n\n\nfunction startsWith(str, search, pos) {\n return str.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search;\n} // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith\n\n\nfunction endsWith(str, search, this_len) {\n if (this_len === undefined || this_len > str.length) {\n this_len = str.length;\n }\n\n return str.substring(this_len - search.length, this_len) === search;\n} // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes\n\n\nfunction includes(str, search, start) {\n if (typeof start !== 'number') {\n start = 0;\n }\n\n if (start + search.length > str.length) {\n return false;\n } else {\n return str.indexOf(search, start) !== -1;\n }\n}\n\ncreateErrorType('ERR_INVALID_OPT_VALUE', function (name, value) {\n return 'The value \"' + value + '\" is invalid for option \"' + name + '\"';\n}, TypeError);\ncreateErrorType('ERR_INVALID_ARG_TYPE', function (name, expected, actual) {\n // determiner: 'must be' or 'must not be'\n var determiner;\n\n if (typeof expected === 'string' && startsWith(expected, 'not ')) {\n determiner = 'must not be';\n expected = expected.replace(/^not /, '');\n } else {\n determiner = 'must be';\n }\n\n var msg;\n\n if (endsWith(name, ' argument')) {\n // For cases like 'first argument'\n msg = \"The \".concat(name, \" \").concat(determiner, \" \").concat(oneOf(expected, 'type'));\n } else {\n var type = includes(name, '.') ? 'property' : 'argument';\n msg = \"The \\\"\".concat(name, \"\\\" \").concat(type, \" \").concat(determiner, \" \").concat(oneOf(expected, 'type'));\n }\n\n msg += \". Received type \".concat(typeof actual);\n return msg;\n}, TypeError);\ncreateErrorType('ERR_STREAM_PUSH_AFTER_EOF', 'stream.push() after EOF');\ncreateErrorType('ERR_METHOD_NOT_IMPLEMENTED', function (name) {\n return 'The ' + name + ' method is not implemented';\n});\ncreateErrorType('ERR_STREAM_PREMATURE_CLOSE', 'Premature close');\ncreateErrorType('ERR_STREAM_DESTROYED', function (name) {\n return 'Cannot call ' + name + ' after a stream was destroyed';\n});\ncreateErrorType('ERR_MULTIPLE_CALLBACK', 'Callback called multiple times');\ncreateErrorType('ERR_STREAM_CANNOT_PIPE', 'Cannot pipe, not readable');\ncreateErrorType('ERR_STREAM_WRITE_AFTER_END', 'write after end');\ncreateErrorType('ERR_STREAM_NULL_VALUES', 'May not write null values to stream', TypeError);\ncreateErrorType('ERR_UNKNOWN_ENCODING', function (arg) {\n return 'Unknown encoding: ' + arg;\n}, TypeError);\ncreateErrorType('ERR_STREAM_UNSHIFT_AFTER_END_EVENT', 'stream.unshift() after end event');\nmodule.exports.codes = codes;\n","// Copyright Joyent, Inc. and other Node contributors.\n//\n// Permission is hereby granted, free of charge, to any person obtaining a\n// copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to permit\n// persons to whom the Software is furnished to do so, subject to the\n// following conditions:\n//\n// The above copyright notice and this permission notice shall be included\n// in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\n// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN\n// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\n// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE\n// USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n// a duplex stream is just a stream that is both readable and writable.\n// Since JS doesn't have multiple prototypal inheritance, this class\n// prototypally inherits from Readable, and then parasitically from\n// Writable.\n\n'use strict';\n\n/**/\nvar objectKeys = Object.keys || function (obj) {\n var keys = [];\n for (var key in obj) keys.push(key);\n return keys;\n};\n/**/\n\nmodule.exports = Duplex;\nvar Readable = require('./_stream_readable');\nvar Writable = require('./_stream_writable');\nrequire('inherits')(Duplex, Readable);\n{\n // Allow the keys array to be GC'ed.\n var keys = objectKeys(Writable.prototype);\n for (var v = 0; v < keys.length; v++) {\n var method = keys[v];\n if (!Duplex.prototype[method]) Duplex.prototype[method] = Writable.prototype[method];\n }\n}\nfunction Duplex(options) {\n if (!(this instanceof Duplex)) return new Duplex(options);\n Readable.call(this, options);\n Writable.call(this, options);\n this.allowHalfOpen = true;\n if (options) {\n if (options.readable === false) this.readable = false;\n if (options.writable === false) this.writable = false;\n if (options.allowHalfOpen === false) {\n this.allowHalfOpen = false;\n this.once('end', onend);\n }\n }\n}\nObject.defineProperty(Duplex.prototype, 'writableHighWaterMark', {\n // making it explicit this property is not enumerable\n // because otherwise some prototype manipulation in\n // userland will fail\n enumerable: false,\n get: function get() {\n return this._writableState.highWaterMark;\n }\n});\nObject.defineProperty(Duplex.prototype, 'writableBuffer', {\n // making it explicit this property is not enumerable\n // because otherwise some prototype manipulation in\n // userland will fail\n enumerable: false,\n get: function get() {\n return this._writableState && this._writableState.getBuffer();\n }\n});\nObject.defineProperty(Duplex.prototype, 'writableLength', {\n // making it explicit this property is not enumerable\n // because otherwise some prototype manipulation in\n // userland will fail\n enumerable: false,\n get: function get() {\n return this._writableState.length;\n }\n});\n\n// the no-half-open enforcer\nfunction onend() {\n // If the writable side ended, then we're ok.\n if (this._writableState.ended) return;\n\n // no more data can be written.\n // But allow more writes to happen in this tick.\n process.nextTick(onEndNT, this);\n}\nfunction onEndNT(self) {\n self.end();\n}\nObject.defineProperty(Duplex.prototype, 'destroyed', {\n // making it explicit this property is not enumerable\n // because otherwise some prototype manipulation in\n // userland will fail\n enumerable: false,\n get: function get() {\n if (this._readableState === undefined || this._writableState === undefined) {\n return false;\n }\n return this._readableState.destroyed && this._writableState.destroyed;\n },\n set: function set(value) {\n // we ignore the value if the stream\n // has not been initialized yet\n if (this._readableState === undefined || this._writableState === undefined) {\n return;\n }\n\n // backward compatibility, the user is explicitly\n // managing destroyed\n this._readableState.destroyed = value;\n this._writableState.destroyed = value;\n }\n});","// Copyright Joyent, Inc. and other Node contributors.\n//\n// Permission is hereby granted, free of charge, to any person obtaining a\n// copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to permit\n// persons to whom the Software is furnished to do so, subject to the\n// following conditions:\n//\n// The above copyright notice and this permission notice shall be included\n// in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\n// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN\n// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\n// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE\n// USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n// a passthrough stream.\n// basically just the most minimal sort of Transform stream.\n// Every written chunk gets output as-is.\n\n'use strict';\n\nmodule.exports = PassThrough;\nvar Transform = require('./_stream_transform');\nrequire('inherits')(PassThrough, Transform);\nfunction PassThrough(options) {\n if (!(this instanceof PassThrough)) return new PassThrough(options);\n Transform.call(this, options);\n}\nPassThrough.prototype._transform = function (chunk, encoding, cb) {\n cb(null, chunk);\n};","// Copyright Joyent, Inc. and other Node contributors.\n//\n// Permission is hereby granted, free of charge, to any person obtaining a\n// copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to permit\n// persons to whom the Software is furnished to do so, subject to the\n// following conditions:\n//\n// The above copyright notice and this permission notice shall be included\n// in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\n// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN\n// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\n// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE\n// USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n'use strict';\n\nmodule.exports = Readable;\n\n/**/\nvar Duplex;\n/**/\n\nReadable.ReadableState = ReadableState;\n\n/**/\nvar EE = require('events').EventEmitter;\nvar EElistenerCount = function EElistenerCount(emitter, type) {\n return emitter.listeners(type).length;\n};\n/**/\n\n/**/\nvar Stream = require('./internal/streams/stream');\n/**/\n\nvar Buffer = require('buffer').Buffer;\nvar OurUint8Array = (typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : {}).Uint8Array || function () {};\nfunction _uint8ArrayToBuffer(chunk) {\n return Buffer.from(chunk);\n}\nfunction _isUint8Array(obj) {\n return Buffer.isBuffer(obj) || obj instanceof OurUint8Array;\n}\n\n/**/\nvar debugUtil = require('util');\nvar debug;\nif (debugUtil && debugUtil.debuglog) {\n debug = debugUtil.debuglog('stream');\n} else {\n debug = function debug() {};\n}\n/**/\n\nvar BufferList = require('./internal/streams/buffer_list');\nvar destroyImpl = require('./internal/streams/destroy');\nvar _require = require('./internal/streams/state'),\n getHighWaterMark = _require.getHighWaterMark;\nvar _require$codes = require('../errors').codes,\n ERR_INVALID_ARG_TYPE = _require$codes.ERR_INVALID_ARG_TYPE,\n ERR_STREAM_PUSH_AFTER_EOF = _require$codes.ERR_STREAM_PUSH_AFTER_EOF,\n ERR_METHOD_NOT_IMPLEMENTED = _require$codes.ERR_METHOD_NOT_IMPLEMENTED,\n ERR_STREAM_UNSHIFT_AFTER_END_EVENT = _require$codes.ERR_STREAM_UNSHIFT_AFTER_END_EVENT;\n\n// Lazy loaded to improve the startup performance.\nvar StringDecoder;\nvar createReadableStreamAsyncIterator;\nvar from;\nrequire('inherits')(Readable, Stream);\nvar errorOrDestroy = destroyImpl.errorOrDestroy;\nvar kProxyEvents = ['error', 'close', 'destroy', 'pause', 'resume'];\nfunction prependListener(emitter, event, fn) {\n // Sadly this is not cacheable as some libraries bundle their own\n // event emitter implementation with them.\n if (typeof emitter.prependListener === 'function') return emitter.prependListener(event, fn);\n\n // This is a hack to make sure that our error handler is attached before any\n // userland ones. NEVER DO THIS. This is here only because this code needs\n // to continue to work with older versions of Node.js that do not include\n // the prependListener() method. The goal is to eventually remove this hack.\n if (!emitter._events || !emitter._events[event]) emitter.on(event, fn);else if (Array.isArray(emitter._events[event])) emitter._events[event].unshift(fn);else emitter._events[event] = [fn, emitter._events[event]];\n}\nfunction ReadableState(options, stream, isDuplex) {\n Duplex = Duplex || require('./_stream_duplex');\n options = options || {};\n\n // Duplex streams are both readable and writable, but share\n // the same options object.\n // However, some cases require setting options to different\n // values for the readable and the writable sides of the duplex stream.\n // These options can be provided separately as readableXXX and writableXXX.\n if (typeof isDuplex !== 'boolean') isDuplex = stream instanceof Duplex;\n\n // object stream flag. Used to make read(n) ignore n and to\n // make all the buffer merging and length checks go away\n this.objectMode = !!options.objectMode;\n if (isDuplex) this.objectMode = this.objectMode || !!options.readableObjectMode;\n\n // the point at which it stops calling _read() to fill the buffer\n // Note: 0 is a valid value, means \"don't call _read preemptively ever\"\n this.highWaterMark = getHighWaterMark(this, options, 'readableHighWaterMark', isDuplex);\n\n // A linked list is used to store data chunks instead of an array because the\n // linked list can remove elements from the beginning faster than\n // array.shift()\n this.buffer = new BufferList();\n this.length = 0;\n this.pipes = null;\n this.pipesCount = 0;\n this.flowing = null;\n this.ended = false;\n this.endEmitted = false;\n this.reading = false;\n\n // a flag to be able to tell if the event 'readable'/'data' is emitted\n // immediately, or on a later tick. We set this to true at first, because\n // any actions that shouldn't happen until \"later\" should generally also\n // not happen before the first read call.\n this.sync = true;\n\n // whenever we return null, then we set a flag to say\n // that we're awaiting a 'readable' event emission.\n this.needReadable = false;\n this.emittedReadable = false;\n this.readableListening = false;\n this.resumeScheduled = false;\n this.paused = true;\n\n // Should close be emitted on destroy. Defaults to true.\n this.emitClose = options.emitClose !== false;\n\n // Should .destroy() be called after 'end' (and potentially 'finish')\n this.autoDestroy = !!options.autoDestroy;\n\n // has it been destroyed\n this.destroyed = false;\n\n // Crypto is kind of old and crusty. Historically, its default string\n // encoding is 'binary' so we have to make this configurable.\n // Everything else in the universe uses 'utf8', though.\n this.defaultEncoding = options.defaultEncoding || 'utf8';\n\n // the number of writers that are awaiting a drain event in .pipe()s\n this.awaitDrain = 0;\n\n // if true, a maybeReadMore has been scheduled\n this.readingMore = false;\n this.decoder = null;\n this.encoding = null;\n if (options.encoding) {\n if (!StringDecoder) StringDecoder = require('string_decoder/').StringDecoder;\n this.decoder = new StringDecoder(options.encoding);\n this.encoding = options.encoding;\n }\n}\nfunction Readable(options) {\n Duplex = Duplex || require('./_stream_duplex');\n if (!(this instanceof Readable)) return new Readable(options);\n\n // Checking for a Stream.Duplex instance is faster here instead of inside\n // the ReadableState constructor, at least with V8 6.5\n var isDuplex = this instanceof Duplex;\n this._readableState = new ReadableState(options, this, isDuplex);\n\n // legacy\n this.readable = true;\n if (options) {\n if (typeof options.read === 'function') this._read = options.read;\n if (typeof options.destroy === 'function') this._destroy = options.destroy;\n }\n Stream.call(this);\n}\nObject.defineProperty(Readable.prototype, 'destroyed', {\n // making it explicit this property is not enumerable\n // because otherwise some prototype manipulation in\n // userland will fail\n enumerable: false,\n get: function get() {\n if (this._readableState === undefined) {\n return false;\n }\n return this._readableState.destroyed;\n },\n set: function set(value) {\n // we ignore the value if the stream\n // has not been initialized yet\n if (!this._readableState) {\n return;\n }\n\n // backward compatibility, the user is explicitly\n // managing destroyed\n this._readableState.destroyed = value;\n }\n});\nReadable.prototype.destroy = destroyImpl.destroy;\nReadable.prototype._undestroy = destroyImpl.undestroy;\nReadable.prototype._destroy = function (err, cb) {\n cb(err);\n};\n\n// Manually shove something into the read() buffer.\n// This returns true if the highWaterMark has not been hit yet,\n// similar to how Writable.write() returns true if you should\n// write() some more.\nReadable.prototype.push = function (chunk, encoding) {\n var state = this._readableState;\n var skipChunkCheck;\n if (!state.objectMode) {\n if (typeof chunk === 'string') {\n encoding = encoding || state.defaultEncoding;\n if (encoding !== state.encoding) {\n chunk = Buffer.from(chunk, encoding);\n encoding = '';\n }\n skipChunkCheck = true;\n }\n } else {\n skipChunkCheck = true;\n }\n return readableAddChunk(this, chunk, encoding, false, skipChunkCheck);\n};\n\n// Unshift should *always* be something directly out of read()\nReadable.prototype.unshift = function (chunk) {\n return readableAddChunk(this, chunk, null, true, false);\n};\nfunction readableAddChunk(stream, chunk, encoding, addToFront, skipChunkCheck) {\n debug('readableAddChunk', chunk);\n var state = stream._readableState;\n if (chunk === null) {\n state.reading = false;\n onEofChunk(stream, state);\n } else {\n var er;\n if (!skipChunkCheck) er = chunkInvalid(state, chunk);\n if (er) {\n errorOrDestroy(stream, er);\n } else if (state.objectMode || chunk && chunk.length > 0) {\n if (typeof chunk !== 'string' && !state.objectMode && Object.getPrototypeOf(chunk) !== Buffer.prototype) {\n chunk = _uint8ArrayToBuffer(chunk);\n }\n if (addToFront) {\n if (state.endEmitted) errorOrDestroy(stream, new ERR_STREAM_UNSHIFT_AFTER_END_EVENT());else addChunk(stream, state, chunk, true);\n } else if (state.ended) {\n errorOrDestroy(stream, new ERR_STREAM_PUSH_AFTER_EOF());\n } else if (state.destroyed) {\n return false;\n } else {\n state.reading = false;\n if (state.decoder && !encoding) {\n chunk = state.decoder.write(chunk);\n if (state.objectMode || chunk.length !== 0) addChunk(stream, state, chunk, false);else maybeReadMore(stream, state);\n } else {\n addChunk(stream, state, chunk, false);\n }\n }\n } else if (!addToFront) {\n state.reading = false;\n maybeReadMore(stream, state);\n }\n }\n\n // We can push more data if we are below the highWaterMark.\n // Also, if we have no data yet, we can stand some more bytes.\n // This is to work around cases where hwm=0, such as the repl.\n return !state.ended && (state.length < state.highWaterMark || state.length === 0);\n}\nfunction addChunk(stream, state, chunk, addToFront) {\n if (state.flowing && state.length === 0 && !state.sync) {\n state.awaitDrain = 0;\n stream.emit('data', chunk);\n } else {\n // update the buffer info.\n state.length += state.objectMode ? 1 : chunk.length;\n if (addToFront) state.buffer.unshift(chunk);else state.buffer.push(chunk);\n if (state.needReadable) emitReadable(stream);\n }\n maybeReadMore(stream, state);\n}\nfunction chunkInvalid(state, chunk) {\n var er;\n if (!_isUint8Array(chunk) && typeof chunk !== 'string' && chunk !== undefined && !state.objectMode) {\n er = new ERR_INVALID_ARG_TYPE('chunk', ['string', 'Buffer', 'Uint8Array'], chunk);\n }\n return er;\n}\nReadable.prototype.isPaused = function () {\n return this._readableState.flowing === false;\n};\n\n// backwards compatibility.\nReadable.prototype.setEncoding = function (enc) {\n if (!StringDecoder) StringDecoder = require('string_decoder/').StringDecoder;\n var decoder = new StringDecoder(enc);\n this._readableState.decoder = decoder;\n // If setEncoding(null), decoder.encoding equals utf8\n this._readableState.encoding = this._readableState.decoder.encoding;\n\n // Iterate over current buffer to convert already stored Buffers:\n var p = this._readableState.buffer.head;\n var content = '';\n while (p !== null) {\n content += decoder.write(p.data);\n p = p.next;\n }\n this._readableState.buffer.clear();\n if (content !== '') this._readableState.buffer.push(content);\n this._readableState.length = content.length;\n return this;\n};\n\n// Don't raise the hwm > 1GB\nvar MAX_HWM = 0x40000000;\nfunction computeNewHighWaterMark(n) {\n if (n >= MAX_HWM) {\n // TODO(ronag): Throw ERR_VALUE_OUT_OF_RANGE.\n n = MAX_HWM;\n } else {\n // Get the next highest power of 2 to prevent increasing hwm excessively in\n // tiny amounts\n n--;\n n |= n >>> 1;\n n |= n >>> 2;\n n |= n >>> 4;\n n |= n >>> 8;\n n |= n >>> 16;\n n++;\n }\n return n;\n}\n\n// This function is designed to be inlinable, so please take care when making\n// changes to the function body.\nfunction howMuchToRead(n, state) {\n if (n <= 0 || state.length === 0 && state.ended) return 0;\n if (state.objectMode) return 1;\n if (n !== n) {\n // Only flow one buffer at a time\n if (state.flowing && state.length) return state.buffer.head.data.length;else return state.length;\n }\n // If we're asking for more than the current hwm, then raise the hwm.\n if (n > state.highWaterMark) state.highWaterMark = computeNewHighWaterMark(n);\n if (n <= state.length) return n;\n // Don't have enough\n if (!state.ended) {\n state.needReadable = true;\n return 0;\n }\n return state.length;\n}\n\n// you can override either this method, or the async _read(n) below.\nReadable.prototype.read = function (n) {\n debug('read', n);\n n = parseInt(n, 10);\n var state = this._readableState;\n var nOrig = n;\n if (n !== 0) state.emittedReadable = false;\n\n // if we're doing read(0) to trigger a readable event, but we\n // already have a bunch of data in the buffer, then just trigger\n // the 'readable' event and move on.\n if (n === 0 && state.needReadable && ((state.highWaterMark !== 0 ? state.length >= state.highWaterMark : state.length > 0) || state.ended)) {\n debug('read: emitReadable', state.length, state.ended);\n if (state.length === 0 && state.ended) endReadable(this);else emitReadable(this);\n return null;\n }\n n = howMuchToRead(n, state);\n\n // if we've ended, and we're now clear, then finish it up.\n if (n === 0 && state.ended) {\n if (state.length === 0) endReadable(this);\n return null;\n }\n\n // All the actual chunk generation logic needs to be\n // *below* the call to _read. The reason is that in certain\n // synthetic stream cases, such as passthrough streams, _read\n // may be a completely synchronous operation which may change\n // the state of the read buffer, providing enough data when\n // before there was *not* enough.\n //\n // So, the steps are:\n // 1. Figure out what the state of things will be after we do\n // a read from the buffer.\n //\n // 2. If that resulting state will trigger a _read, then call _read.\n // Note that this may be asynchronous, or synchronous. Yes, it is\n // deeply ugly to write APIs this way, but that still doesn't mean\n // that the Readable class should behave improperly, as streams are\n // designed to be sync/async agnostic.\n // Take note if the _read call is sync or async (ie, if the read call\n // has returned yet), so that we know whether or not it's safe to emit\n // 'readable' etc.\n //\n // 3. Actually pull the requested chunks out of the buffer and return.\n\n // if we need a readable event, then we need to do some reading.\n var doRead = state.needReadable;\n debug('need readable', doRead);\n\n // if we currently have less than the highWaterMark, then also read some\n if (state.length === 0 || state.length - n < state.highWaterMark) {\n doRead = true;\n debug('length less than watermark', doRead);\n }\n\n // however, if we've ended, then there's no point, and if we're already\n // reading, then it's unnecessary.\n if (state.ended || state.reading) {\n doRead = false;\n debug('reading or ended', doRead);\n } else if (doRead) {\n debug('do read');\n state.reading = true;\n state.sync = true;\n // if the length is currently zero, then we *need* a readable event.\n if (state.length === 0) state.needReadable = true;\n // call internal read method\n this._read(state.highWaterMark);\n state.sync = false;\n // If _read pushed data synchronously, then `reading` will be false,\n // and we need to re-evaluate how much data we can return to the user.\n if (!state.reading) n = howMuchToRead(nOrig, state);\n }\n var ret;\n if (n > 0) ret = fromList(n, state);else ret = null;\n if (ret === null) {\n state.needReadable = state.length <= state.highWaterMark;\n n = 0;\n } else {\n state.length -= n;\n state.awaitDrain = 0;\n }\n if (state.length === 0) {\n // If we have nothing in the buffer, then we want to know\n // as soon as we *do* get something into the buffer.\n if (!state.ended) state.needReadable = true;\n\n // If we tried to read() past the EOF, then emit end on the next tick.\n if (nOrig !== n && state.ended) endReadable(this);\n }\n if (ret !== null) this.emit('data', ret);\n return ret;\n};\nfunction onEofChunk(stream, state) {\n debug('onEofChunk');\n if (state.ended) return;\n if (state.decoder) {\n var chunk = state.decoder.end();\n if (chunk && chunk.length) {\n state.buffer.push(chunk);\n state.length += state.objectMode ? 1 : chunk.length;\n }\n }\n state.ended = true;\n if (state.sync) {\n // if we are sync, wait until next tick to emit the data.\n // Otherwise we risk emitting data in the flow()\n // the readable code triggers during a read() call\n emitReadable(stream);\n } else {\n // emit 'readable' now to make sure it gets picked up.\n state.needReadable = false;\n if (!state.emittedReadable) {\n state.emittedReadable = true;\n emitReadable_(stream);\n }\n }\n}\n\n// Don't emit readable right away in sync mode, because this can trigger\n// another read() call => stack overflow. This way, it might trigger\n// a nextTick recursion warning, but that's not so bad.\nfunction emitReadable(stream) {\n var state = stream._readableState;\n debug('emitReadable', state.needReadable, state.emittedReadable);\n state.needReadable = false;\n if (!state.emittedReadable) {\n debug('emitReadable', state.flowing);\n state.emittedReadable = true;\n process.nextTick(emitReadable_, stream);\n }\n}\nfunction emitReadable_(stream) {\n var state = stream._readableState;\n debug('emitReadable_', state.destroyed, state.length, state.ended);\n if (!state.destroyed && (state.length || state.ended)) {\n stream.emit('readable');\n state.emittedReadable = false;\n }\n\n // The stream needs another readable event if\n // 1. It is not flowing, as the flow mechanism will take\n // care of it.\n // 2. It is not ended.\n // 3. It is below the highWaterMark, so we can schedule\n // another readable later.\n state.needReadable = !state.flowing && !state.ended && state.length <= state.highWaterMark;\n flow(stream);\n}\n\n// at this point, the user has presumably seen the 'readable' event,\n// and called read() to consume some data. that may have triggered\n// in turn another _read(n) call, in which case reading = true if\n// it's in progress.\n// However, if we're not ended, or reading, and the length < hwm,\n// then go ahead and try to read some more preemptively.\nfunction maybeReadMore(stream, state) {\n if (!state.readingMore) {\n state.readingMore = true;\n process.nextTick(maybeReadMore_, stream, state);\n }\n}\nfunction maybeReadMore_(stream, state) {\n // Attempt to read more data if we should.\n //\n // The conditions for reading more data are (one of):\n // - Not enough data buffered (state.length < state.highWaterMark). The loop\n // is responsible for filling the buffer with enough data if such data\n // is available. If highWaterMark is 0 and we are not in the flowing mode\n // we should _not_ attempt to buffer any extra data. We'll get more data\n // when the stream consumer calls read() instead.\n // - No data in the buffer, and the stream is in flowing mode. In this mode\n // the loop below is responsible for ensuring read() is called. Failing to\n // call read here would abort the flow and there's no other mechanism for\n // continuing the flow if the stream consumer has just subscribed to the\n // 'data' event.\n //\n // In addition to the above conditions to keep reading data, the following\n // conditions prevent the data from being read:\n // - The stream has ended (state.ended).\n // - There is already a pending 'read' operation (state.reading). This is a\n // case where the the stream has called the implementation defined _read()\n // method, but they are processing the call asynchronously and have _not_\n // called push() with new data. In this case we skip performing more\n // read()s. The execution ends in this method again after the _read() ends\n // up calling push() with more data.\n while (!state.reading && !state.ended && (state.length < state.highWaterMark || state.flowing && state.length === 0)) {\n var len = state.length;\n debug('maybeReadMore read 0');\n stream.read(0);\n if (len === state.length)\n // didn't get any data, stop spinning.\n break;\n }\n state.readingMore = false;\n}\n\n// abstract method. to be overridden in specific implementation classes.\n// call cb(er, data) where data is <= n in length.\n// for virtual (non-string, non-buffer) streams, \"length\" is somewhat\n// arbitrary, and perhaps not very meaningful.\nReadable.prototype._read = function (n) {\n errorOrDestroy(this, new ERR_METHOD_NOT_IMPLEMENTED('_read()'));\n};\nReadable.prototype.pipe = function (dest, pipeOpts) {\n var src = this;\n var state = this._readableState;\n switch (state.pipesCount) {\n case 0:\n state.pipes = dest;\n break;\n case 1:\n state.pipes = [state.pipes, dest];\n break;\n default:\n state.pipes.push(dest);\n break;\n }\n state.pipesCount += 1;\n debug('pipe count=%d opts=%j', state.pipesCount, pipeOpts);\n var doEnd = (!pipeOpts || pipeOpts.end !== false) && dest !== process.stdout && dest !== process.stderr;\n var endFn = doEnd ? onend : unpipe;\n if (state.endEmitted) process.nextTick(endFn);else src.once('end', endFn);\n dest.on('unpipe', onunpipe);\n function onunpipe(readable, unpipeInfo) {\n debug('onunpipe');\n if (readable === src) {\n if (unpipeInfo && unpipeInfo.hasUnpiped === false) {\n unpipeInfo.hasUnpiped = true;\n cleanup();\n }\n }\n }\n function onend() {\n debug('onend');\n dest.end();\n }\n\n // when the dest drains, it reduces the awaitDrain counter\n // on the source. This would be more elegant with a .once()\n // handler in flow(), but adding and removing repeatedly is\n // too slow.\n var ondrain = pipeOnDrain(src);\n dest.on('drain', ondrain);\n var cleanedUp = false;\n function cleanup() {\n debug('cleanup');\n // cleanup event handlers once the pipe is broken\n dest.removeListener('close', onclose);\n dest.removeListener('finish', onfinish);\n dest.removeListener('drain', ondrain);\n dest.removeListener('error', onerror);\n dest.removeListener('unpipe', onunpipe);\n src.removeListener('end', onend);\n src.removeListener('end', unpipe);\n src.removeListener('data', ondata);\n cleanedUp = true;\n\n // if the reader is waiting for a drain event from this\n // specific writer, then it would cause it to never start\n // flowing again.\n // So, if this is awaiting a drain, then we just call it now.\n // If we don't know, then assume that we are waiting for one.\n if (state.awaitDrain && (!dest._writableState || dest._writableState.needDrain)) ondrain();\n }\n src.on('data', ondata);\n function ondata(chunk) {\n debug('ondata');\n var ret = dest.write(chunk);\n debug('dest.write', ret);\n if (ret === false) {\n // If the user unpiped during `dest.write()`, it is possible\n // to get stuck in a permanently paused state if that write\n // also returned false.\n // => Check whether `dest` is still a piping destination.\n if ((state.pipesCount === 1 && state.pipes === dest || state.pipesCount > 1 && indexOf(state.pipes, dest) !== -1) && !cleanedUp) {\n debug('false write response, pause', state.awaitDrain);\n state.awaitDrain++;\n }\n src.pause();\n }\n }\n\n // if the dest has an error, then stop piping into it.\n // however, don't suppress the throwing behavior for this.\n function onerror(er) {\n debug('onerror', er);\n unpipe();\n dest.removeListener('error', onerror);\n if (EElistenerCount(dest, 'error') === 0) errorOrDestroy(dest, er);\n }\n\n // Make sure our error handler is attached before userland ones.\n prependListener(dest, 'error', onerror);\n\n // Both close and finish should trigger unpipe, but only once.\n function onclose() {\n dest.removeListener('finish', onfinish);\n unpipe();\n }\n dest.once('close', onclose);\n function onfinish() {\n debug('onfinish');\n dest.removeListener('close', onclose);\n unpipe();\n }\n dest.once('finish', onfinish);\n function unpipe() {\n debug('unpipe');\n src.unpipe(dest);\n }\n\n // tell the dest that it's being piped to\n dest.emit('pipe', src);\n\n // start the flow if it hasn't been started already.\n if (!state.flowing) {\n debug('pipe resume');\n src.resume();\n }\n return dest;\n};\nfunction pipeOnDrain(src) {\n return function pipeOnDrainFunctionResult() {\n var state = src._readableState;\n debug('pipeOnDrain', state.awaitDrain);\n if (state.awaitDrain) state.awaitDrain--;\n if (state.awaitDrain === 0 && EElistenerCount(src, 'data')) {\n state.flowing = true;\n flow(src);\n }\n };\n}\nReadable.prototype.unpipe = function (dest) {\n var state = this._readableState;\n var unpipeInfo = {\n hasUnpiped: false\n };\n\n // if we're not piping anywhere, then do nothing.\n if (state.pipesCount === 0) return this;\n\n // just one destination. most common case.\n if (state.pipesCount === 1) {\n // passed in one, but it's not the right one.\n if (dest && dest !== state.pipes) return this;\n if (!dest) dest = state.pipes;\n\n // got a match.\n state.pipes = null;\n state.pipesCount = 0;\n state.flowing = false;\n if (dest) dest.emit('unpipe', this, unpipeInfo);\n return this;\n }\n\n // slow case. multiple pipe destinations.\n\n if (!dest) {\n // remove all.\n var dests = state.pipes;\n var len = state.pipesCount;\n state.pipes = null;\n state.pipesCount = 0;\n state.flowing = false;\n for (var i = 0; i < len; i++) dests[i].emit('unpipe', this, {\n hasUnpiped: false\n });\n return this;\n }\n\n // try to find the right one.\n var index = indexOf(state.pipes, dest);\n if (index === -1) return this;\n state.pipes.splice(index, 1);\n state.pipesCount -= 1;\n if (state.pipesCount === 1) state.pipes = state.pipes[0];\n dest.emit('unpipe', this, unpipeInfo);\n return this;\n};\n\n// set up data events if they are asked for\n// Ensure readable listeners eventually get something\nReadable.prototype.on = function (ev, fn) {\n var res = Stream.prototype.on.call(this, ev, fn);\n var state = this._readableState;\n if (ev === 'data') {\n // update readableListening so that resume() may be a no-op\n // a few lines down. This is needed to support once('readable').\n state.readableListening = this.listenerCount('readable') > 0;\n\n // Try start flowing on next tick if stream isn't explicitly paused\n if (state.flowing !== false) this.resume();\n } else if (ev === 'readable') {\n if (!state.endEmitted && !state.readableListening) {\n state.readableListening = state.needReadable = true;\n state.flowing = false;\n state.emittedReadable = false;\n debug('on readable', state.length, state.reading);\n if (state.length) {\n emitReadable(this);\n } else if (!state.reading) {\n process.nextTick(nReadingNextTick, this);\n }\n }\n }\n return res;\n};\nReadable.prototype.addListener = Readable.prototype.on;\nReadable.prototype.removeListener = function (ev, fn) {\n var res = Stream.prototype.removeListener.call(this, ev, fn);\n if (ev === 'readable') {\n // We need to check if there is someone still listening to\n // readable and reset the state. However this needs to happen\n // after readable has been emitted but before I/O (nextTick) to\n // support once('readable', fn) cycles. This means that calling\n // resume within the same tick will have no\n // effect.\n process.nextTick(updateReadableListening, this);\n }\n return res;\n};\nReadable.prototype.removeAllListeners = function (ev) {\n var res = Stream.prototype.removeAllListeners.apply(this, arguments);\n if (ev === 'readable' || ev === undefined) {\n // We need to check if there is someone still listening to\n // readable and reset the state. However this needs to happen\n // after readable has been emitted but before I/O (nextTick) to\n // support once('readable', fn) cycles. This means that calling\n // resume within the same tick will have no\n // effect.\n process.nextTick(updateReadableListening, this);\n }\n return res;\n};\nfunction updateReadableListening(self) {\n var state = self._readableState;\n state.readableListening = self.listenerCount('readable') > 0;\n if (state.resumeScheduled && !state.paused) {\n // flowing needs to be set to true now, otherwise\n // the upcoming resume will not flow.\n state.flowing = true;\n\n // crude way to check if we should resume\n } else if (self.listenerCount('data') > 0) {\n self.resume();\n }\n}\nfunction nReadingNextTick(self) {\n debug('readable nexttick read 0');\n self.read(0);\n}\n\n// pause() and resume() are remnants of the legacy readable stream API\n// If the user uses them, then switch into old mode.\nReadable.prototype.resume = function () {\n var state = this._readableState;\n if (!state.flowing) {\n debug('resume');\n // we flow only if there is no one listening\n // for readable, but we still have to call\n // resume()\n state.flowing = !state.readableListening;\n resume(this, state);\n }\n state.paused = false;\n return this;\n};\nfunction resume(stream, state) {\n if (!state.resumeScheduled) {\n state.resumeScheduled = true;\n process.nextTick(resume_, stream, state);\n }\n}\nfunction resume_(stream, state) {\n debug('resume', state.reading);\n if (!state.reading) {\n stream.read(0);\n }\n state.resumeScheduled = false;\n stream.emit('resume');\n flow(stream);\n if (state.flowing && !state.reading) stream.read(0);\n}\nReadable.prototype.pause = function () {\n debug('call pause flowing=%j', this._readableState.flowing);\n if (this._readableState.flowing !== false) {\n debug('pause');\n this._readableState.flowing = false;\n this.emit('pause');\n }\n this._readableState.paused = true;\n return this;\n};\nfunction flow(stream) {\n var state = stream._readableState;\n debug('flow', state.flowing);\n while (state.flowing && stream.read() !== null);\n}\n\n// wrap an old-style stream as the async data source.\n// This is *not* part of the readable stream interface.\n// It is an ugly unfortunate mess of history.\nReadable.prototype.wrap = function (stream) {\n var _this = this;\n var state = this._readableState;\n var paused = false;\n stream.on('end', function () {\n debug('wrapped end');\n if (state.decoder && !state.ended) {\n var chunk = state.decoder.end();\n if (chunk && chunk.length) _this.push(chunk);\n }\n _this.push(null);\n });\n stream.on('data', function (chunk) {\n debug('wrapped data');\n if (state.decoder) chunk = state.decoder.write(chunk);\n\n // don't skip over falsy values in objectMode\n if (state.objectMode && (chunk === null || chunk === undefined)) return;else if (!state.objectMode && (!chunk || !chunk.length)) return;\n var ret = _this.push(chunk);\n if (!ret) {\n paused = true;\n stream.pause();\n }\n });\n\n // proxy all the other methods.\n // important when wrapping filters and duplexes.\n for (var i in stream) {\n if (this[i] === undefined && typeof stream[i] === 'function') {\n this[i] = function methodWrap(method) {\n return function methodWrapReturnFunction() {\n return stream[method].apply(stream, arguments);\n };\n }(i);\n }\n }\n\n // proxy certain important events.\n for (var n = 0; n < kProxyEvents.length; n++) {\n stream.on(kProxyEvents[n], this.emit.bind(this, kProxyEvents[n]));\n }\n\n // when we try to consume some more bytes, simply unpause the\n // underlying stream.\n this._read = function (n) {\n debug('wrapped _read', n);\n if (paused) {\n paused = false;\n stream.resume();\n }\n };\n return this;\n};\nif (typeof Symbol === 'function') {\n Readable.prototype[Symbol.asyncIterator] = function () {\n if (createReadableStreamAsyncIterator === undefined) {\n createReadableStreamAsyncIterator = require('./internal/streams/async_iterator');\n }\n return createReadableStreamAsyncIterator(this);\n };\n}\nObject.defineProperty(Readable.prototype, 'readableHighWaterMark', {\n // making it explicit this property is not enumerable\n // because otherwise some prototype manipulation in\n // userland will fail\n enumerable: false,\n get: function get() {\n return this._readableState.highWaterMark;\n }\n});\nObject.defineProperty(Readable.prototype, 'readableBuffer', {\n // making it explicit this property is not enumerable\n // because otherwise some prototype manipulation in\n // userland will fail\n enumerable: false,\n get: function get() {\n return this._readableState && this._readableState.buffer;\n }\n});\nObject.defineProperty(Readable.prototype, 'readableFlowing', {\n // making it explicit this property is not enumerable\n // because otherwise some prototype manipulation in\n // userland will fail\n enumerable: false,\n get: function get() {\n return this._readableState.flowing;\n },\n set: function set(state) {\n if (this._readableState) {\n this._readableState.flowing = state;\n }\n }\n});\n\n// exposed for testing purposes only.\nReadable._fromList = fromList;\nObject.defineProperty(Readable.prototype, 'readableLength', {\n // making it explicit this property is not enumerable\n // because otherwise some prototype manipulation in\n // userland will fail\n enumerable: false,\n get: function get() {\n return this._readableState.length;\n }\n});\n\n// Pluck off n bytes from an array of buffers.\n// Length is the combined lengths of all the buffers in the list.\n// This function is designed to be inlinable, so please take care when making\n// changes to the function body.\nfunction fromList(n, state) {\n // nothing buffered\n if (state.length === 0) return null;\n var ret;\n if (state.objectMode) ret = state.buffer.shift();else if (!n || n >= state.length) {\n // read it all, truncate the list\n if (state.decoder) ret = state.buffer.join('');else if (state.buffer.length === 1) ret = state.buffer.first();else ret = state.buffer.concat(state.length);\n state.buffer.clear();\n } else {\n // read part of list\n ret = state.buffer.consume(n, state.decoder);\n }\n return ret;\n}\nfunction endReadable(stream) {\n var state = stream._readableState;\n debug('endReadable', state.endEmitted);\n if (!state.endEmitted) {\n state.ended = true;\n process.nextTick(endReadableNT, state, stream);\n }\n}\nfunction endReadableNT(state, stream) {\n debug('endReadableNT', state.endEmitted, state.length);\n\n // Check that we didn't get one last unshift.\n if (!state.endEmitted && state.length === 0) {\n state.endEmitted = true;\n stream.readable = false;\n stream.emit('end');\n if (state.autoDestroy) {\n // In case of duplex streams we need a way to detect\n // if the writable side is ready for autoDestroy as well\n var wState = stream._writableState;\n if (!wState || wState.autoDestroy && wState.finished) {\n stream.destroy();\n }\n }\n }\n}\nif (typeof Symbol === 'function') {\n Readable.from = function (iterable, opts) {\n if (from === undefined) {\n from = require('./internal/streams/from');\n }\n return from(Readable, iterable, opts);\n };\n}\nfunction indexOf(xs, x) {\n for (var i = 0, l = xs.length; i < l; i++) {\n if (xs[i] === x) return i;\n }\n return -1;\n}","// Copyright Joyent, Inc. and other Node contributors.\n//\n// Permission is hereby granted, free of charge, to any person obtaining a\n// copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to permit\n// persons to whom the Software is furnished to do so, subject to the\n// following conditions:\n//\n// The above copyright notice and this permission notice shall be included\n// in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\n// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN\n// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\n// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE\n// USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n// a transform stream is a readable/writable stream where you do\n// something with the data. Sometimes it's called a \"filter\",\n// but that's not a great name for it, since that implies a thing where\n// some bits pass through, and others are simply ignored. (That would\n// be a valid example of a transform, of course.)\n//\n// While the output is causally related to the input, it's not a\n// necessarily symmetric or synchronous transformation. For example,\n// a zlib stream might take multiple plain-text writes(), and then\n// emit a single compressed chunk some time in the future.\n//\n// Here's how this works:\n//\n// The Transform stream has all the aspects of the readable and writable\n// stream classes. When you write(chunk), that calls _write(chunk,cb)\n// internally, and returns false if there's a lot of pending writes\n// buffered up. When you call read(), that calls _read(n) until\n// there's enough pending readable data buffered up.\n//\n// In a transform stream, the written data is placed in a buffer. When\n// _read(n) is called, it transforms the queued up data, calling the\n// buffered _write cb's as it consumes chunks. If consuming a single\n// written chunk would result in multiple output chunks, then the first\n// outputted bit calls the readcb, and subsequent chunks just go into\n// the read buffer, and will cause it to emit 'readable' if necessary.\n//\n// This way, back-pressure is actually determined by the reading side,\n// since _read has to be called to start processing a new chunk. However,\n// a pathological inflate type of transform can cause excessive buffering\n// here. For example, imagine a stream where every byte of input is\n// interpreted as an integer from 0-255, and then results in that many\n// bytes of output. Writing the 4 bytes {ff,ff,ff,ff} would result in\n// 1kb of data being output. In this case, you could write a very small\n// amount of input, and end up with a very large amount of output. In\n// such a pathological inflating mechanism, there'd be no way to tell\n// the system to stop doing the transform. A single 4MB write could\n// cause the system to run out of memory.\n//\n// However, even in such a pathological case, only a single written chunk\n// would be consumed, and then the rest would wait (un-transformed) until\n// the results of the previous transformed chunk were consumed.\n\n'use strict';\n\nmodule.exports = Transform;\nvar _require$codes = require('../errors').codes,\n ERR_METHOD_NOT_IMPLEMENTED = _require$codes.ERR_METHOD_NOT_IMPLEMENTED,\n ERR_MULTIPLE_CALLBACK = _require$codes.ERR_MULTIPLE_CALLBACK,\n ERR_TRANSFORM_ALREADY_TRANSFORMING = _require$codes.ERR_TRANSFORM_ALREADY_TRANSFORMING,\n ERR_TRANSFORM_WITH_LENGTH_0 = _require$codes.ERR_TRANSFORM_WITH_LENGTH_0;\nvar Duplex = require('./_stream_duplex');\nrequire('inherits')(Transform, Duplex);\nfunction afterTransform(er, data) {\n var ts = this._transformState;\n ts.transforming = false;\n var cb = ts.writecb;\n if (cb === null) {\n return this.emit('error', new ERR_MULTIPLE_CALLBACK());\n }\n ts.writechunk = null;\n ts.writecb = null;\n if (data != null)\n // single equals check for both `null` and `undefined`\n this.push(data);\n cb(er);\n var rs = this._readableState;\n rs.reading = false;\n if (rs.needReadable || rs.length < rs.highWaterMark) {\n this._read(rs.highWaterMark);\n }\n}\nfunction Transform(options) {\n if (!(this instanceof Transform)) return new Transform(options);\n Duplex.call(this, options);\n this._transformState = {\n afterTransform: afterTransform.bind(this),\n needTransform: false,\n transforming: false,\n writecb: null,\n writechunk: null,\n writeencoding: null\n };\n\n // start out asking for a readable event once data is transformed.\n this._readableState.needReadable = true;\n\n // we have implemented the _read method, and done the other things\n // that Readable wants before the first _read call, so unset the\n // sync guard flag.\n this._readableState.sync = false;\n if (options) {\n if (typeof options.transform === 'function') this._transform = options.transform;\n if (typeof options.flush === 'function') this._flush = options.flush;\n }\n\n // When the writable side finishes, then flush out anything remaining.\n this.on('prefinish', prefinish);\n}\nfunction prefinish() {\n var _this = this;\n if (typeof this._flush === 'function' && !this._readableState.destroyed) {\n this._flush(function (er, data) {\n done(_this, er, data);\n });\n } else {\n done(this, null, null);\n }\n}\nTransform.prototype.push = function (chunk, encoding) {\n this._transformState.needTransform = false;\n return Duplex.prototype.push.call(this, chunk, encoding);\n};\n\n// This is the part where you do stuff!\n// override this function in implementation classes.\n// 'chunk' is an input chunk.\n//\n// Call `push(newChunk)` to pass along transformed output\n// to the readable side. You may call 'push' zero or more times.\n//\n// Call `cb(err)` when you are done with this chunk. If you pass\n// an error, then that'll put the hurt on the whole operation. If you\n// never call cb(), then you'll never get another chunk.\nTransform.prototype._transform = function (chunk, encoding, cb) {\n cb(new ERR_METHOD_NOT_IMPLEMENTED('_transform()'));\n};\nTransform.prototype._write = function (chunk, encoding, cb) {\n var ts = this._transformState;\n ts.writecb = cb;\n ts.writechunk = chunk;\n ts.writeencoding = encoding;\n if (!ts.transforming) {\n var rs = this._readableState;\n if (ts.needTransform || rs.needReadable || rs.length < rs.highWaterMark) this._read(rs.highWaterMark);\n }\n};\n\n// Doesn't matter what the args are here.\n// _transform does all the work.\n// That we got here means that the readable side wants more data.\nTransform.prototype._read = function (n) {\n var ts = this._transformState;\n if (ts.writechunk !== null && !ts.transforming) {\n ts.transforming = true;\n this._transform(ts.writechunk, ts.writeencoding, ts.afterTransform);\n } else {\n // mark that we need a transform, so that any data that comes in\n // will get processed, now that we've asked for it.\n ts.needTransform = true;\n }\n};\nTransform.prototype._destroy = function (err, cb) {\n Duplex.prototype._destroy.call(this, err, function (err2) {\n cb(err2);\n });\n};\nfunction done(stream, er, data) {\n if (er) return stream.emit('error', er);\n if (data != null)\n // single equals check for both `null` and `undefined`\n stream.push(data);\n\n // TODO(BridgeAR): Write a test for these two error cases\n // if there's nothing in the write buffer, then that means\n // that nothing more will ever be provided\n if (stream._writableState.length) throw new ERR_TRANSFORM_WITH_LENGTH_0();\n if (stream._transformState.transforming) throw new ERR_TRANSFORM_ALREADY_TRANSFORMING();\n return stream.push(null);\n}","// Copyright Joyent, Inc. and other Node contributors.\n//\n// Permission is hereby granted, free of charge, to any person obtaining a\n// copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to permit\n// persons to whom the Software is furnished to do so, subject to the\n// following conditions:\n//\n// The above copyright notice and this permission notice shall be included\n// in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\n// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN\n// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\n// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE\n// USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n// A bit simpler than readable streams.\n// Implement an async ._write(chunk, encoding, cb), and it'll handle all\n// the drain event emission and buffering.\n\n'use strict';\n\nmodule.exports = Writable;\n\n/* */\nfunction WriteReq(chunk, encoding, cb) {\n this.chunk = chunk;\n this.encoding = encoding;\n this.callback = cb;\n this.next = null;\n}\n\n// It seems a linked list but it is not\n// there will be only 2 of these for each stream\nfunction CorkedRequest(state) {\n var _this = this;\n this.next = null;\n this.entry = null;\n this.finish = function () {\n onCorkedFinish(_this, state);\n };\n}\n/* */\n\n/**/\nvar Duplex;\n/**/\n\nWritable.WritableState = WritableState;\n\n/**/\nvar internalUtil = {\n deprecate: require('util-deprecate')\n};\n/**/\n\n/**/\nvar Stream = require('./internal/streams/stream');\n/**/\n\nvar Buffer = require('buffer').Buffer;\nvar OurUint8Array = (typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : {}).Uint8Array || function () {};\nfunction _uint8ArrayToBuffer(chunk) {\n return Buffer.from(chunk);\n}\nfunction _isUint8Array(obj) {\n return Buffer.isBuffer(obj) || obj instanceof OurUint8Array;\n}\nvar destroyImpl = require('./internal/streams/destroy');\nvar _require = require('./internal/streams/state'),\n getHighWaterMark = _require.getHighWaterMark;\nvar _require$codes = require('../errors').codes,\n ERR_INVALID_ARG_TYPE = _require$codes.ERR_INVALID_ARG_TYPE,\n ERR_METHOD_NOT_IMPLEMENTED = _require$codes.ERR_METHOD_NOT_IMPLEMENTED,\n ERR_MULTIPLE_CALLBACK = _require$codes.ERR_MULTIPLE_CALLBACK,\n ERR_STREAM_CANNOT_PIPE = _require$codes.ERR_STREAM_CANNOT_PIPE,\n ERR_STREAM_DESTROYED = _require$codes.ERR_STREAM_DESTROYED,\n ERR_STREAM_NULL_VALUES = _require$codes.ERR_STREAM_NULL_VALUES,\n ERR_STREAM_WRITE_AFTER_END = _require$codes.ERR_STREAM_WRITE_AFTER_END,\n ERR_UNKNOWN_ENCODING = _require$codes.ERR_UNKNOWN_ENCODING;\nvar errorOrDestroy = destroyImpl.errorOrDestroy;\nrequire('inherits')(Writable, Stream);\nfunction nop() {}\nfunction WritableState(options, stream, isDuplex) {\n Duplex = Duplex || require('./_stream_duplex');\n options = options || {};\n\n // Duplex streams are both readable and writable, but share\n // the same options object.\n // However, some cases require setting options to different\n // values for the readable and the writable sides of the duplex stream,\n // e.g. options.readableObjectMode vs. options.writableObjectMode, etc.\n if (typeof isDuplex !== 'boolean') isDuplex = stream instanceof Duplex;\n\n // object stream flag to indicate whether or not this stream\n // contains buffers or objects.\n this.objectMode = !!options.objectMode;\n if (isDuplex) this.objectMode = this.objectMode || !!options.writableObjectMode;\n\n // the point at which write() starts returning false\n // Note: 0 is a valid value, means that we always return false if\n // the entire buffer is not flushed immediately on write()\n this.highWaterMark = getHighWaterMark(this, options, 'writableHighWaterMark', isDuplex);\n\n // if _final has been called\n this.finalCalled = false;\n\n // drain event flag.\n this.needDrain = false;\n // at the start of calling end()\n this.ending = false;\n // when end() has been called, and returned\n this.ended = false;\n // when 'finish' is emitted\n this.finished = false;\n\n // has it been destroyed\n this.destroyed = false;\n\n // should we decode strings into buffers before passing to _write?\n // this is here so that some node-core streams can optimize string\n // handling at a lower level.\n var noDecode = options.decodeStrings === false;\n this.decodeStrings = !noDecode;\n\n // Crypto is kind of old and crusty. Historically, its default string\n // encoding is 'binary' so we have to make this configurable.\n // Everything else in the universe uses 'utf8', though.\n this.defaultEncoding = options.defaultEncoding || 'utf8';\n\n // not an actual buffer we keep track of, but a measurement\n // of how much we're waiting to get pushed to some underlying\n // socket or file.\n this.length = 0;\n\n // a flag to see when we're in the middle of a write.\n this.writing = false;\n\n // when true all writes will be buffered until .uncork() call\n this.corked = 0;\n\n // a flag to be able to tell if the onwrite cb is called immediately,\n // or on a later tick. We set this to true at first, because any\n // actions that shouldn't happen until \"later\" should generally also\n // not happen before the first write call.\n this.sync = true;\n\n // a flag to know if we're processing previously buffered items, which\n // may call the _write() callback in the same tick, so that we don't\n // end up in an overlapped onwrite situation.\n this.bufferProcessing = false;\n\n // the callback that's passed to _write(chunk,cb)\n this.onwrite = function (er) {\n onwrite(stream, er);\n };\n\n // the callback that the user supplies to write(chunk,encoding,cb)\n this.writecb = null;\n\n // the amount that is being written when _write is called.\n this.writelen = 0;\n this.bufferedRequest = null;\n this.lastBufferedRequest = null;\n\n // number of pending user-supplied write callbacks\n // this must be 0 before 'finish' can be emitted\n this.pendingcb = 0;\n\n // emit prefinish if the only thing we're waiting for is _write cbs\n // This is relevant for synchronous Transform streams\n this.prefinished = false;\n\n // True if the error was already emitted and should not be thrown again\n this.errorEmitted = false;\n\n // Should close be emitted on destroy. Defaults to true.\n this.emitClose = options.emitClose !== false;\n\n // Should .destroy() be called after 'finish' (and potentially 'end')\n this.autoDestroy = !!options.autoDestroy;\n\n // count buffered requests\n this.bufferedRequestCount = 0;\n\n // allocate the first CorkedRequest, there is always\n // one allocated and free to use, and we maintain at most two\n this.corkedRequestsFree = new CorkedRequest(this);\n}\nWritableState.prototype.getBuffer = function getBuffer() {\n var current = this.bufferedRequest;\n var out = [];\n while (current) {\n out.push(current);\n current = current.next;\n }\n return out;\n};\n(function () {\n try {\n Object.defineProperty(WritableState.prototype, 'buffer', {\n get: internalUtil.deprecate(function writableStateBufferGetter() {\n return this.getBuffer();\n }, '_writableState.buffer is deprecated. Use _writableState.getBuffer ' + 'instead.', 'DEP0003')\n });\n } catch (_) {}\n})();\n\n// Test _writableState for inheritance to account for Duplex streams,\n// whose prototype chain only points to Readable.\nvar realHasInstance;\nif (typeof Symbol === 'function' && Symbol.hasInstance && typeof Function.prototype[Symbol.hasInstance] === 'function') {\n realHasInstance = Function.prototype[Symbol.hasInstance];\n Object.defineProperty(Writable, Symbol.hasInstance, {\n value: function value(object) {\n if (realHasInstance.call(this, object)) return true;\n if (this !== Writable) return false;\n return object && object._writableState instanceof WritableState;\n }\n });\n} else {\n realHasInstance = function realHasInstance(object) {\n return object instanceof this;\n };\n}\nfunction Writable(options) {\n Duplex = Duplex || require('./_stream_duplex');\n\n // Writable ctor is applied to Duplexes, too.\n // `realHasInstance` is necessary because using plain `instanceof`\n // would return false, as no `_writableState` property is attached.\n\n // Trying to use the custom `instanceof` for Writable here will also break the\n // Node.js LazyTransform implementation, which has a non-trivial getter for\n // `_writableState` that would lead to infinite recursion.\n\n // Checking for a Stream.Duplex instance is faster here instead of inside\n // the WritableState constructor, at least with V8 6.5\n var isDuplex = this instanceof Duplex;\n if (!isDuplex && !realHasInstance.call(Writable, this)) return new Writable(options);\n this._writableState = new WritableState(options, this, isDuplex);\n\n // legacy.\n this.writable = true;\n if (options) {\n if (typeof options.write === 'function') this._write = options.write;\n if (typeof options.writev === 'function') this._writev = options.writev;\n if (typeof options.destroy === 'function') this._destroy = options.destroy;\n if (typeof options.final === 'function') this._final = options.final;\n }\n Stream.call(this);\n}\n\n// Otherwise people can pipe Writable streams, which is just wrong.\nWritable.prototype.pipe = function () {\n errorOrDestroy(this, new ERR_STREAM_CANNOT_PIPE());\n};\nfunction writeAfterEnd(stream, cb) {\n var er = new ERR_STREAM_WRITE_AFTER_END();\n // TODO: defer error events consistently everywhere, not just the cb\n errorOrDestroy(stream, er);\n process.nextTick(cb, er);\n}\n\n// Checks that a user-supplied chunk is valid, especially for the particular\n// mode the stream is in. Currently this means that `null` is never accepted\n// and undefined/non-string values are only allowed in object mode.\nfunction validChunk(stream, state, chunk, cb) {\n var er;\n if (chunk === null) {\n er = new ERR_STREAM_NULL_VALUES();\n } else if (typeof chunk !== 'string' && !state.objectMode) {\n er = new ERR_INVALID_ARG_TYPE('chunk', ['string', 'Buffer'], chunk);\n }\n if (er) {\n errorOrDestroy(stream, er);\n process.nextTick(cb, er);\n return false;\n }\n return true;\n}\nWritable.prototype.write = function (chunk, encoding, cb) {\n var state = this._writableState;\n var ret = false;\n var isBuf = !state.objectMode && _isUint8Array(chunk);\n if (isBuf && !Buffer.isBuffer(chunk)) {\n chunk = _uint8ArrayToBuffer(chunk);\n }\n if (typeof encoding === 'function') {\n cb = encoding;\n encoding = null;\n }\n if (isBuf) encoding = 'buffer';else if (!encoding) encoding = state.defaultEncoding;\n if (typeof cb !== 'function') cb = nop;\n if (state.ending) writeAfterEnd(this, cb);else if (isBuf || validChunk(this, state, chunk, cb)) {\n state.pendingcb++;\n ret = writeOrBuffer(this, state, isBuf, chunk, encoding, cb);\n }\n return ret;\n};\nWritable.prototype.cork = function () {\n this._writableState.corked++;\n};\nWritable.prototype.uncork = function () {\n var state = this._writableState;\n if (state.corked) {\n state.corked--;\n if (!state.writing && !state.corked && !state.bufferProcessing && state.bufferedRequest) clearBuffer(this, state);\n }\n};\nWritable.prototype.setDefaultEncoding = function setDefaultEncoding(encoding) {\n // node::ParseEncoding() requires lower case.\n if (typeof encoding === 'string') encoding = encoding.toLowerCase();\n if (!(['hex', 'utf8', 'utf-8', 'ascii', 'binary', 'base64', 'ucs2', 'ucs-2', 'utf16le', 'utf-16le', 'raw'].indexOf((encoding + '').toLowerCase()) > -1)) throw new ERR_UNKNOWN_ENCODING(encoding);\n this._writableState.defaultEncoding = encoding;\n return this;\n};\nObject.defineProperty(Writable.prototype, 'writableBuffer', {\n // making it explicit this property is not enumerable\n // because otherwise some prototype manipulation in\n // userland will fail\n enumerable: false,\n get: function get() {\n return this._writableState && this._writableState.getBuffer();\n }\n});\nfunction decodeChunk(state, chunk, encoding) {\n if (!state.objectMode && state.decodeStrings !== false && typeof chunk === 'string') {\n chunk = Buffer.from(chunk, encoding);\n }\n return chunk;\n}\nObject.defineProperty(Writable.prototype, 'writableHighWaterMark', {\n // making it explicit this property is not enumerable\n // because otherwise some prototype manipulation in\n // userland will fail\n enumerable: false,\n get: function get() {\n return this._writableState.highWaterMark;\n }\n});\n\n// if we're already writing something, then just put this\n// in the queue, and wait our turn. Otherwise, call _write\n// If we return false, then we need a drain event, so set that flag.\nfunction writeOrBuffer(stream, state, isBuf, chunk, encoding, cb) {\n if (!isBuf) {\n var newChunk = decodeChunk(state, chunk, encoding);\n if (chunk !== newChunk) {\n isBuf = true;\n encoding = 'buffer';\n chunk = newChunk;\n }\n }\n var len = state.objectMode ? 1 : chunk.length;\n state.length += len;\n var ret = state.length < state.highWaterMark;\n // we must ensure that previous needDrain will not be reset to false.\n if (!ret) state.needDrain = true;\n if (state.writing || state.corked) {\n var last = state.lastBufferedRequest;\n state.lastBufferedRequest = {\n chunk: chunk,\n encoding: encoding,\n isBuf: isBuf,\n callback: cb,\n next: null\n };\n if (last) {\n last.next = state.lastBufferedRequest;\n } else {\n state.bufferedRequest = state.lastBufferedRequest;\n }\n state.bufferedRequestCount += 1;\n } else {\n doWrite(stream, state, false, len, chunk, encoding, cb);\n }\n return ret;\n}\nfunction doWrite(stream, state, writev, len, chunk, encoding, cb) {\n state.writelen = len;\n state.writecb = cb;\n state.writing = true;\n state.sync = true;\n if (state.destroyed) state.onwrite(new ERR_STREAM_DESTROYED('write'));else if (writev) stream._writev(chunk, state.onwrite);else stream._write(chunk, encoding, state.onwrite);\n state.sync = false;\n}\nfunction onwriteError(stream, state, sync, er, cb) {\n --state.pendingcb;\n if (sync) {\n // defer the callback if we are being called synchronously\n // to avoid piling up things on the stack\n process.nextTick(cb, er);\n // this can emit finish, and it will always happen\n // after error\n process.nextTick(finishMaybe, stream, state);\n stream._writableState.errorEmitted = true;\n errorOrDestroy(stream, er);\n } else {\n // the caller expect this to happen before if\n // it is async\n cb(er);\n stream._writableState.errorEmitted = true;\n errorOrDestroy(stream, er);\n // this can emit finish, but finish must\n // always follow error\n finishMaybe(stream, state);\n }\n}\nfunction onwriteStateUpdate(state) {\n state.writing = false;\n state.writecb = null;\n state.length -= state.writelen;\n state.writelen = 0;\n}\nfunction onwrite(stream, er) {\n var state = stream._writableState;\n var sync = state.sync;\n var cb = state.writecb;\n if (typeof cb !== 'function') throw new ERR_MULTIPLE_CALLBACK();\n onwriteStateUpdate(state);\n if (er) onwriteError(stream, state, sync, er, cb);else {\n // Check if we're actually ready to finish, but don't emit yet\n var finished = needFinish(state) || stream.destroyed;\n if (!finished && !state.corked && !state.bufferProcessing && state.bufferedRequest) {\n clearBuffer(stream, state);\n }\n if (sync) {\n process.nextTick(afterWrite, stream, state, finished, cb);\n } else {\n afterWrite(stream, state, finished, cb);\n }\n }\n}\nfunction afterWrite(stream, state, finished, cb) {\n if (!finished) onwriteDrain(stream, state);\n state.pendingcb--;\n cb();\n finishMaybe(stream, state);\n}\n\n// Must force callback to be called on nextTick, so that we don't\n// emit 'drain' before the write() consumer gets the 'false' return\n// value, and has a chance to attach a 'drain' listener.\nfunction onwriteDrain(stream, state) {\n if (state.length === 0 && state.needDrain) {\n state.needDrain = false;\n stream.emit('drain');\n }\n}\n\n// if there's something in the buffer waiting, then process it\nfunction clearBuffer(stream, state) {\n state.bufferProcessing = true;\n var entry = state.bufferedRequest;\n if (stream._writev && entry && entry.next) {\n // Fast case, write everything using _writev()\n var l = state.bufferedRequestCount;\n var buffer = new Array(l);\n var holder = state.corkedRequestsFree;\n holder.entry = entry;\n var count = 0;\n var allBuffers = true;\n while (entry) {\n buffer[count] = entry;\n if (!entry.isBuf) allBuffers = false;\n entry = entry.next;\n count += 1;\n }\n buffer.allBuffers = allBuffers;\n doWrite(stream, state, true, state.length, buffer, '', holder.finish);\n\n // doWrite is almost always async, defer these to save a bit of time\n // as the hot path ends with doWrite\n state.pendingcb++;\n state.lastBufferedRequest = null;\n if (holder.next) {\n state.corkedRequestsFree = holder.next;\n holder.next = null;\n } else {\n state.corkedRequestsFree = new CorkedRequest(state);\n }\n state.bufferedRequestCount = 0;\n } else {\n // Slow case, write chunks one-by-one\n while (entry) {\n var chunk = entry.chunk;\n var encoding = entry.encoding;\n var cb = entry.callback;\n var len = state.objectMode ? 1 : chunk.length;\n doWrite(stream, state, false, len, chunk, encoding, cb);\n entry = entry.next;\n state.bufferedRequestCount--;\n // if we didn't call the onwrite immediately, then\n // it means that we need to wait until it does.\n // also, that means that the chunk and cb are currently\n // being processed, so move the buffer counter past them.\n if (state.writing) {\n break;\n }\n }\n if (entry === null) state.lastBufferedRequest = null;\n }\n state.bufferedRequest = entry;\n state.bufferProcessing = false;\n}\nWritable.prototype._write = function (chunk, encoding, cb) {\n cb(new ERR_METHOD_NOT_IMPLEMENTED('_write()'));\n};\nWritable.prototype._writev = null;\nWritable.prototype.end = function (chunk, encoding, cb) {\n var state = this._writableState;\n if (typeof chunk === 'function') {\n cb = chunk;\n chunk = null;\n encoding = null;\n } else if (typeof encoding === 'function') {\n cb = encoding;\n encoding = null;\n }\n if (chunk !== null && chunk !== undefined) this.write(chunk, encoding);\n\n // .end() fully uncorks\n if (state.corked) {\n state.corked = 1;\n this.uncork();\n }\n\n // ignore unnecessary end() calls.\n if (!state.ending) endWritable(this, state, cb);\n return this;\n};\nObject.defineProperty(Writable.prototype, 'writableLength', {\n // making it explicit this property is not enumerable\n // because otherwise some prototype manipulation in\n // userland will fail\n enumerable: false,\n get: function get() {\n return this._writableState.length;\n }\n});\nfunction needFinish(state) {\n return state.ending && state.length === 0 && state.bufferedRequest === null && !state.finished && !state.writing;\n}\nfunction callFinal(stream, state) {\n stream._final(function (err) {\n state.pendingcb--;\n if (err) {\n errorOrDestroy(stream, err);\n }\n state.prefinished = true;\n stream.emit('prefinish');\n finishMaybe(stream, state);\n });\n}\nfunction prefinish(stream, state) {\n if (!state.prefinished && !state.finalCalled) {\n if (typeof stream._final === 'function' && !state.destroyed) {\n state.pendingcb++;\n state.finalCalled = true;\n process.nextTick(callFinal, stream, state);\n } else {\n state.prefinished = true;\n stream.emit('prefinish');\n }\n }\n}\nfunction finishMaybe(stream, state) {\n var need = needFinish(state);\n if (need) {\n prefinish(stream, state);\n if (state.pendingcb === 0) {\n state.finished = true;\n stream.emit('finish');\n if (state.autoDestroy) {\n // In case of duplex streams we need a way to detect\n // if the readable side is ready for autoDestroy as well\n var rState = stream._readableState;\n if (!rState || rState.autoDestroy && rState.endEmitted) {\n stream.destroy();\n }\n }\n }\n }\n return need;\n}\nfunction endWritable(stream, state, cb) {\n state.ending = true;\n finishMaybe(stream, state);\n if (cb) {\n if (state.finished) process.nextTick(cb);else stream.once('finish', cb);\n }\n state.ended = true;\n stream.writable = false;\n}\nfunction onCorkedFinish(corkReq, state, err) {\n var entry = corkReq.entry;\n corkReq.entry = null;\n while (entry) {\n var cb = entry.callback;\n state.pendingcb--;\n cb(err);\n entry = entry.next;\n }\n\n // reuse the free corkReq.\n state.corkedRequestsFree.next = corkReq;\n}\nObject.defineProperty(Writable.prototype, 'destroyed', {\n // making it explicit this property is not enumerable\n // because otherwise some prototype manipulation in\n // userland will fail\n enumerable: false,\n get: function get() {\n if (this._writableState === undefined) {\n return false;\n }\n return this._writableState.destroyed;\n },\n set: function set(value) {\n // we ignore the value if the stream\n // has not been initialized yet\n if (!this._writableState) {\n return;\n }\n\n // backward compatibility, the user is explicitly\n // managing destroyed\n this._writableState.destroyed = value;\n }\n});\nWritable.prototype.destroy = destroyImpl.destroy;\nWritable.prototype._undestroy = destroyImpl.undestroy;\nWritable.prototype._destroy = function (err, cb) {\n cb(err);\n};","'use strict';\n\nvar _Object$setPrototypeO;\nfunction _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }\nfunction _toPropertyKey(arg) { var key = _toPrimitive(arg, \"string\"); return typeof key === \"symbol\" ? key : String(key); }\nfunction _toPrimitive(input, hint) { if (typeof input !== \"object\" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || \"default\"); if (typeof res !== \"object\") return res; throw new TypeError(\"@@toPrimitive must return a primitive value.\"); } return (hint === \"string\" ? String : Number)(input); }\nvar finished = require('./end-of-stream');\nvar kLastResolve = Symbol('lastResolve');\nvar kLastReject = Symbol('lastReject');\nvar kError = Symbol('error');\nvar kEnded = Symbol('ended');\nvar kLastPromise = Symbol('lastPromise');\nvar kHandlePromise = Symbol('handlePromise');\nvar kStream = Symbol('stream');\nfunction createIterResult(value, done) {\n return {\n value: value,\n done: done\n };\n}\nfunction readAndResolve(iter) {\n var resolve = iter[kLastResolve];\n if (resolve !== null) {\n var data = iter[kStream].read();\n // we defer if data is null\n // we can be expecting either 'end' or\n // 'error'\n if (data !== null) {\n iter[kLastPromise] = null;\n iter[kLastResolve] = null;\n iter[kLastReject] = null;\n resolve(createIterResult(data, false));\n }\n }\n}\nfunction onReadable(iter) {\n // we wait for the next tick, because it might\n // emit an error with process.nextTick\n process.nextTick(readAndResolve, iter);\n}\nfunction wrapForNext(lastPromise, iter) {\n return function (resolve, reject) {\n lastPromise.then(function () {\n if (iter[kEnded]) {\n resolve(createIterResult(undefined, true));\n return;\n }\n iter[kHandlePromise](resolve, reject);\n }, reject);\n };\n}\nvar AsyncIteratorPrototype = Object.getPrototypeOf(function () {});\nvar ReadableStreamAsyncIteratorPrototype = Object.setPrototypeOf((_Object$setPrototypeO = {\n get stream() {\n return this[kStream];\n },\n next: function next() {\n var _this = this;\n // if we have detected an error in the meanwhile\n // reject straight away\n var error = this[kError];\n if (error !== null) {\n return Promise.reject(error);\n }\n if (this[kEnded]) {\n return Promise.resolve(createIterResult(undefined, true));\n }\n if (this[kStream].destroyed) {\n // We need to defer via nextTick because if .destroy(err) is\n // called, the error will be emitted via nextTick, and\n // we cannot guarantee that there is no error lingering around\n // waiting to be emitted.\n return new Promise(function (resolve, reject) {\n process.nextTick(function () {\n if (_this[kError]) {\n reject(_this[kError]);\n } else {\n resolve(createIterResult(undefined, true));\n }\n });\n });\n }\n\n // if we have multiple next() calls\n // we will wait for the previous Promise to finish\n // this logic is optimized to support for await loops,\n // where next() is only called once at a time\n var lastPromise = this[kLastPromise];\n var promise;\n if (lastPromise) {\n promise = new Promise(wrapForNext(lastPromise, this));\n } else {\n // fast path needed to support multiple this.push()\n // without triggering the next() queue\n var data = this[kStream].read();\n if (data !== null) {\n return Promise.resolve(createIterResult(data, false));\n }\n promise = new Promise(this[kHandlePromise]);\n }\n this[kLastPromise] = promise;\n return promise;\n }\n}, _defineProperty(_Object$setPrototypeO, Symbol.asyncIterator, function () {\n return this;\n}), _defineProperty(_Object$setPrototypeO, \"return\", function _return() {\n var _this2 = this;\n // destroy(err, cb) is a private API\n // we can guarantee we have that here, because we control the\n // Readable class this is attached to\n return new Promise(function (resolve, reject) {\n _this2[kStream].destroy(null, function (err) {\n if (err) {\n reject(err);\n return;\n }\n resolve(createIterResult(undefined, true));\n });\n });\n}), _Object$setPrototypeO), AsyncIteratorPrototype);\nvar createReadableStreamAsyncIterator = function createReadableStreamAsyncIterator(stream) {\n var _Object$create;\n var iterator = Object.create(ReadableStreamAsyncIteratorPrototype, (_Object$create = {}, _defineProperty(_Object$create, kStream, {\n value: stream,\n writable: true\n }), _defineProperty(_Object$create, kLastResolve, {\n value: null,\n writable: true\n }), _defineProperty(_Object$create, kLastReject, {\n value: null,\n writable: true\n }), _defineProperty(_Object$create, kError, {\n value: null,\n writable: true\n }), _defineProperty(_Object$create, kEnded, {\n value: stream._readableState.endEmitted,\n writable: true\n }), _defineProperty(_Object$create, kHandlePromise, {\n value: function value(resolve, reject) {\n var data = iterator[kStream].read();\n if (data) {\n iterator[kLastPromise] = null;\n iterator[kLastResolve] = null;\n iterator[kLastReject] = null;\n resolve(createIterResult(data, false));\n } else {\n iterator[kLastResolve] = resolve;\n iterator[kLastReject] = reject;\n }\n },\n writable: true\n }), _Object$create));\n iterator[kLastPromise] = null;\n finished(stream, function (err) {\n if (err && err.code !== 'ERR_STREAM_PREMATURE_CLOSE') {\n var reject = iterator[kLastReject];\n // reject if we are waiting for data in the Promise\n // returned by next() and store the error\n if (reject !== null) {\n iterator[kLastPromise] = null;\n iterator[kLastResolve] = null;\n iterator[kLastReject] = null;\n reject(err);\n }\n iterator[kError] = err;\n return;\n }\n var resolve = iterator[kLastResolve];\n if (resolve !== null) {\n iterator[kLastPromise] = null;\n iterator[kLastResolve] = null;\n iterator[kLastReject] = null;\n resolve(createIterResult(undefined, true));\n }\n iterator[kEnded] = true;\n });\n stream.on('readable', onReadable.bind(null, iterator));\n return iterator;\n};\nmodule.exports = createReadableStreamAsyncIterator;","'use strict';\n\nfunction ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }\nfunction _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }\nfunction _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, \"prototype\", { writable: false }); return Constructor; }\nfunction _toPropertyKey(arg) { var key = _toPrimitive(arg, \"string\"); return typeof key === \"symbol\" ? key : String(key); }\nfunction _toPrimitive(input, hint) { if (typeof input !== \"object\" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || \"default\"); if (typeof res !== \"object\") return res; throw new TypeError(\"@@toPrimitive must return a primitive value.\"); } return (hint === \"string\" ? String : Number)(input); }\nvar _require = require('buffer'),\n Buffer = _require.Buffer;\nvar _require2 = require('util'),\n inspect = _require2.inspect;\nvar custom = inspect && inspect.custom || 'inspect';\nfunction copyBuffer(src, target, offset) {\n Buffer.prototype.copy.call(src, target, offset);\n}\nmodule.exports = /*#__PURE__*/function () {\n function BufferList() {\n _classCallCheck(this, BufferList);\n this.head = null;\n this.tail = null;\n this.length = 0;\n }\n _createClass(BufferList, [{\n key: \"push\",\n value: function push(v) {\n var entry = {\n data: v,\n next: null\n };\n if (this.length > 0) this.tail.next = entry;else this.head = entry;\n this.tail = entry;\n ++this.length;\n }\n }, {\n key: \"unshift\",\n value: function unshift(v) {\n var entry = {\n data: v,\n next: this.head\n };\n if (this.length === 0) this.tail = entry;\n this.head = entry;\n ++this.length;\n }\n }, {\n key: \"shift\",\n value: function shift() {\n if (this.length === 0) return;\n var ret = this.head.data;\n if (this.length === 1) this.head = this.tail = null;else this.head = this.head.next;\n --this.length;\n return ret;\n }\n }, {\n key: \"clear\",\n value: function clear() {\n this.head = this.tail = null;\n this.length = 0;\n }\n }, {\n key: \"join\",\n value: function join(s) {\n if (this.length === 0) return '';\n var p = this.head;\n var ret = '' + p.data;\n while (p = p.next) ret += s + p.data;\n return ret;\n }\n }, {\n key: \"concat\",\n value: function concat(n) {\n if (this.length === 0) return Buffer.alloc(0);\n var ret = Buffer.allocUnsafe(n >>> 0);\n var p = this.head;\n var i = 0;\n while (p) {\n copyBuffer(p.data, ret, i);\n i += p.data.length;\n p = p.next;\n }\n return ret;\n }\n\n // Consumes a specified amount of bytes or characters from the buffered data.\n }, {\n key: \"consume\",\n value: function consume(n, hasStrings) {\n var ret;\n if (n < this.head.data.length) {\n // `slice` is the same for buffers and strings.\n ret = this.head.data.slice(0, n);\n this.head.data = this.head.data.slice(n);\n } else if (n === this.head.data.length) {\n // First chunk is a perfect match.\n ret = this.shift();\n } else {\n // Result spans more than one buffer.\n ret = hasStrings ? this._getString(n) : this._getBuffer(n);\n }\n return ret;\n }\n }, {\n key: \"first\",\n value: function first() {\n return this.head.data;\n }\n\n // Consumes a specified amount of characters from the buffered data.\n }, {\n key: \"_getString\",\n value: function _getString(n) {\n var p = this.head;\n var c = 1;\n var ret = p.data;\n n -= ret.length;\n while (p = p.next) {\n var str = p.data;\n var nb = n > str.length ? str.length : n;\n if (nb === str.length) ret += str;else ret += str.slice(0, n);\n n -= nb;\n if (n === 0) {\n if (nb === str.length) {\n ++c;\n if (p.next) this.head = p.next;else this.head = this.tail = null;\n } else {\n this.head = p;\n p.data = str.slice(nb);\n }\n break;\n }\n ++c;\n }\n this.length -= c;\n return ret;\n }\n\n // Consumes a specified amount of bytes from the buffered data.\n }, {\n key: \"_getBuffer\",\n value: function _getBuffer(n) {\n var ret = Buffer.allocUnsafe(n);\n var p = this.head;\n var c = 1;\n p.data.copy(ret);\n n -= p.data.length;\n while (p = p.next) {\n var buf = p.data;\n var nb = n > buf.length ? buf.length : n;\n buf.copy(ret, ret.length - n, 0, nb);\n n -= nb;\n if (n === 0) {\n if (nb === buf.length) {\n ++c;\n if (p.next) this.head = p.next;else this.head = this.tail = null;\n } else {\n this.head = p;\n p.data = buf.slice(nb);\n }\n break;\n }\n ++c;\n }\n this.length -= c;\n return ret;\n }\n\n // Make sure the linked list only shows the minimal necessary information.\n }, {\n key: custom,\n value: function value(_, options) {\n return inspect(this, _objectSpread(_objectSpread({}, options), {}, {\n // Only inspect one level.\n depth: 0,\n // It should not recurse.\n customInspect: false\n }));\n }\n }]);\n return BufferList;\n}();","'use strict';\n\n// undocumented cb() API, needed for core, not for public API\nfunction destroy(err, cb) {\n var _this = this;\n var readableDestroyed = this._readableState && this._readableState.destroyed;\n var writableDestroyed = this._writableState && this._writableState.destroyed;\n if (readableDestroyed || writableDestroyed) {\n if (cb) {\n cb(err);\n } else if (err) {\n if (!this._writableState) {\n process.nextTick(emitErrorNT, this, err);\n } else if (!this._writableState.errorEmitted) {\n this._writableState.errorEmitted = true;\n process.nextTick(emitErrorNT, this, err);\n }\n }\n return this;\n }\n\n // we set destroyed to true before firing error callbacks in order\n // to make it re-entrance safe in case destroy() is called within callbacks\n\n if (this._readableState) {\n this._readableState.destroyed = true;\n }\n\n // if this is a duplex stream mark the writable part as destroyed as well\n if (this._writableState) {\n this._writableState.destroyed = true;\n }\n this._destroy(err || null, function (err) {\n if (!cb && err) {\n if (!_this._writableState) {\n process.nextTick(emitErrorAndCloseNT, _this, err);\n } else if (!_this._writableState.errorEmitted) {\n _this._writableState.errorEmitted = true;\n process.nextTick(emitErrorAndCloseNT, _this, err);\n } else {\n process.nextTick(emitCloseNT, _this);\n }\n } else if (cb) {\n process.nextTick(emitCloseNT, _this);\n cb(err);\n } else {\n process.nextTick(emitCloseNT, _this);\n }\n });\n return this;\n}\nfunction emitErrorAndCloseNT(self, err) {\n emitErrorNT(self, err);\n emitCloseNT(self);\n}\nfunction emitCloseNT(self) {\n if (self._writableState && !self._writableState.emitClose) return;\n if (self._readableState && !self._readableState.emitClose) return;\n self.emit('close');\n}\nfunction undestroy() {\n if (this._readableState) {\n this._readableState.destroyed = false;\n this._readableState.reading = false;\n this._readableState.ended = false;\n this._readableState.endEmitted = false;\n }\n if (this._writableState) {\n this._writableState.destroyed = false;\n this._writableState.ended = false;\n this._writableState.ending = false;\n this._writableState.finalCalled = false;\n this._writableState.prefinished = false;\n this._writableState.finished = false;\n this._writableState.errorEmitted = false;\n }\n}\nfunction emitErrorNT(self, err) {\n self.emit('error', err);\n}\nfunction errorOrDestroy(stream, err) {\n // We have tests that rely on errors being emitted\n // in the same tick, so changing this is semver major.\n // For now when you opt-in to autoDestroy we allow\n // the error to be emitted nextTick. In a future\n // semver major update we should change the default to this.\n\n var rState = stream._readableState;\n var wState = stream._writableState;\n if (rState && rState.autoDestroy || wState && wState.autoDestroy) stream.destroy(err);else stream.emit('error', err);\n}\nmodule.exports = {\n destroy: destroy,\n undestroy: undestroy,\n errorOrDestroy: errorOrDestroy\n};","// Ported from https://github.com/mafintosh/end-of-stream with\n// permission from the author, Mathias Buus (@mafintosh).\n\n'use strict';\n\nvar ERR_STREAM_PREMATURE_CLOSE = require('../../../errors').codes.ERR_STREAM_PREMATURE_CLOSE;\nfunction once(callback) {\n var called = false;\n return function () {\n if (called) return;\n called = true;\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n callback.apply(this, args);\n };\n}\nfunction noop() {}\nfunction isRequest(stream) {\n return stream.setHeader && typeof stream.abort === 'function';\n}\nfunction eos(stream, opts, callback) {\n if (typeof opts === 'function') return eos(stream, null, opts);\n if (!opts) opts = {};\n callback = once(callback || noop);\n var readable = opts.readable || opts.readable !== false && stream.readable;\n var writable = opts.writable || opts.writable !== false && stream.writable;\n var onlegacyfinish = function onlegacyfinish() {\n if (!stream.writable) onfinish();\n };\n var writableEnded = stream._writableState && stream._writableState.finished;\n var onfinish = function onfinish() {\n writable = false;\n writableEnded = true;\n if (!readable) callback.call(stream);\n };\n var readableEnded = stream._readableState && stream._readableState.endEmitted;\n var onend = function onend() {\n readable = false;\n readableEnded = true;\n if (!writable) callback.call(stream);\n };\n var onerror = function onerror(err) {\n callback.call(stream, err);\n };\n var onclose = function onclose() {\n var err;\n if (readable && !readableEnded) {\n if (!stream._readableState || !stream._readableState.ended) err = new ERR_STREAM_PREMATURE_CLOSE();\n return callback.call(stream, err);\n }\n if (writable && !writableEnded) {\n if (!stream._writableState || !stream._writableState.ended) err = new ERR_STREAM_PREMATURE_CLOSE();\n return callback.call(stream, err);\n }\n };\n var onrequest = function onrequest() {\n stream.req.on('finish', onfinish);\n };\n if (isRequest(stream)) {\n stream.on('complete', onfinish);\n stream.on('abort', onclose);\n if (stream.req) onrequest();else stream.on('request', onrequest);\n } else if (writable && !stream._writableState) {\n // legacy streams\n stream.on('end', onlegacyfinish);\n stream.on('close', onlegacyfinish);\n }\n stream.on('end', onend);\n stream.on('finish', onfinish);\n if (opts.error !== false) stream.on('error', onerror);\n stream.on('close', onclose);\n return function () {\n stream.removeListener('complete', onfinish);\n stream.removeListener('abort', onclose);\n stream.removeListener('request', onrequest);\n if (stream.req) stream.req.removeListener('finish', onfinish);\n stream.removeListener('end', onlegacyfinish);\n stream.removeListener('close', onlegacyfinish);\n stream.removeListener('finish', onfinish);\n stream.removeListener('end', onend);\n stream.removeListener('error', onerror);\n stream.removeListener('close', onclose);\n };\n}\nmodule.exports = eos;","module.exports = function () {\n throw new Error('Readable.from is not available in the browser')\n};\n","// Ported from https://github.com/mafintosh/pump with\n// permission from the author, Mathias Buus (@mafintosh).\n\n'use strict';\n\nvar eos;\nfunction once(callback) {\n var called = false;\n return function () {\n if (called) return;\n called = true;\n callback.apply(void 0, arguments);\n };\n}\nvar _require$codes = require('../../../errors').codes,\n ERR_MISSING_ARGS = _require$codes.ERR_MISSING_ARGS,\n ERR_STREAM_DESTROYED = _require$codes.ERR_STREAM_DESTROYED;\nfunction noop(err) {\n // Rethrow the error if it exists to avoid swallowing it\n if (err) throw err;\n}\nfunction isRequest(stream) {\n return stream.setHeader && typeof stream.abort === 'function';\n}\nfunction destroyer(stream, reading, writing, callback) {\n callback = once(callback);\n var closed = false;\n stream.on('close', function () {\n closed = true;\n });\n if (eos === undefined) eos = require('./end-of-stream');\n eos(stream, {\n readable: reading,\n writable: writing\n }, function (err) {\n if (err) return callback(err);\n closed = true;\n callback();\n });\n var destroyed = false;\n return function (err) {\n if (closed) return;\n if (destroyed) return;\n destroyed = true;\n\n // request.destroy just do .end - .abort is what we want\n if (isRequest(stream)) return stream.abort();\n if (typeof stream.destroy === 'function') return stream.destroy();\n callback(err || new ERR_STREAM_DESTROYED('pipe'));\n };\n}\nfunction call(fn) {\n fn();\n}\nfunction pipe(from, to) {\n return from.pipe(to);\n}\nfunction popCallback(streams) {\n if (!streams.length) return noop;\n if (typeof streams[streams.length - 1] !== 'function') return noop;\n return streams.pop();\n}\nfunction pipeline() {\n for (var _len = arguments.length, streams = new Array(_len), _key = 0; _key < _len; _key++) {\n streams[_key] = arguments[_key];\n }\n var callback = popCallback(streams);\n if (Array.isArray(streams[0])) streams = streams[0];\n if (streams.length < 2) {\n throw new ERR_MISSING_ARGS('streams');\n }\n var error;\n var destroys = streams.map(function (stream, i) {\n var reading = i < streams.length - 1;\n var writing = i > 0;\n return destroyer(stream, reading, writing, function (err) {\n if (!error) error = err;\n if (err) destroys.forEach(call);\n if (reading) return;\n destroys.forEach(call);\n callback(error);\n });\n });\n return streams.reduce(pipe);\n}\nmodule.exports = pipeline;","'use strict';\n\nvar ERR_INVALID_OPT_VALUE = require('../../../errors').codes.ERR_INVALID_OPT_VALUE;\nfunction highWaterMarkFrom(options, isDuplex, duplexKey) {\n return options.highWaterMark != null ? options.highWaterMark : isDuplex ? options[duplexKey] : null;\n}\nfunction getHighWaterMark(state, options, duplexKey, isDuplex) {\n var hwm = highWaterMarkFrom(options, isDuplex, duplexKey);\n if (hwm != null) {\n if (!(isFinite(hwm) && Math.floor(hwm) === hwm) || hwm < 0) {\n var name = isDuplex ? duplexKey : 'highWaterMark';\n throw new ERR_INVALID_OPT_VALUE(name, hwm);\n }\n return Math.floor(hwm);\n }\n\n // Default value\n return state.objectMode ? 16 : 16 * 1024;\n}\nmodule.exports = {\n getHighWaterMark: getHighWaterMark\n};","module.exports = require('events').EventEmitter;\n","/*! safe-buffer. MIT License. Feross Aboukhadijeh */\n/* eslint-disable node/no-deprecated-api */\nvar buffer = require('buffer')\nvar Buffer = buffer.Buffer\n\n// alternative to using Object.keys for old browsers\nfunction copyProps (src, dst) {\n for (var key in src) {\n dst[key] = src[key]\n }\n}\nif (Buffer.from && Buffer.alloc && Buffer.allocUnsafe && Buffer.allocUnsafeSlow) {\n module.exports = buffer\n} else {\n // Copy properties from require('buffer')\n copyProps(buffer, exports)\n exports.Buffer = SafeBuffer\n}\n\nfunction SafeBuffer (arg, encodingOrOffset, length) {\n return Buffer(arg, encodingOrOffset, length)\n}\n\nSafeBuffer.prototype = Object.create(Buffer.prototype)\n\n// Copy static methods from Buffer\ncopyProps(Buffer, SafeBuffer)\n\nSafeBuffer.from = function (arg, encodingOrOffset, length) {\n if (typeof arg === 'number') {\n throw new TypeError('Argument must not be a number')\n }\n return Buffer(arg, encodingOrOffset, length)\n}\n\nSafeBuffer.alloc = function (size, fill, encoding) {\n if (typeof size !== 'number') {\n throw new TypeError('Argument must be a number')\n }\n var buf = Buffer(size)\n if (fill !== undefined) {\n if (typeof encoding === 'string') {\n buf.fill(fill, encoding)\n } else {\n buf.fill(fill)\n }\n } else {\n buf.fill(0)\n }\n return buf\n}\n\nSafeBuffer.allocUnsafe = function (size) {\n if (typeof size !== 'number') {\n throw new TypeError('Argument must be a number')\n }\n return Buffer(size)\n}\n\nSafeBuffer.allocUnsafeSlow = function (size) {\n if (typeof size !== 'number') {\n throw new TypeError('Argument must be a number')\n }\n return buffer.SlowBuffer(size)\n}\n","var Buffer = require('safe-buffer').Buffer\n\n// prototype class for hash functions\nfunction Hash (blockSize, finalSize) {\n this._block = Buffer.alloc(blockSize)\n this._finalSize = finalSize\n this._blockSize = blockSize\n this._len = 0\n}\n\nHash.prototype.update = function (data, enc) {\n if (typeof data === 'string') {\n enc = enc || 'utf8'\n data = Buffer.from(data, enc)\n }\n\n var block = this._block\n var blockSize = this._blockSize\n var length = data.length\n var accum = this._len\n\n for (var offset = 0; offset < length;) {\n var assigned = accum % blockSize\n var remainder = Math.min(length - offset, blockSize - assigned)\n\n for (var i = 0; i < remainder; i++) {\n block[assigned + i] = data[offset + i]\n }\n\n accum += remainder\n offset += remainder\n\n if ((accum % blockSize) === 0) {\n this._update(block)\n }\n }\n\n this._len += length\n return this\n}\n\nHash.prototype.digest = function (enc) {\n var rem = this._len % this._blockSize\n\n this._block[rem] = 0x80\n\n // zero (rem + 1) trailing bits, where (rem + 1) is the smallest\n // non-negative solution to the equation (length + 1 + (rem + 1)) === finalSize mod blockSize\n this._block.fill(0, rem + 1)\n\n if (rem >= this._finalSize) {\n this._update(this._block)\n this._block.fill(0)\n }\n\n var bits = this._len * 8\n\n // uint32\n if (bits <= 0xffffffff) {\n this._block.writeUInt32BE(bits, this._blockSize - 4)\n\n // uint64\n } else {\n var lowBits = (bits & 0xffffffff) >>> 0\n var highBits = (bits - lowBits) / 0x100000000\n\n this._block.writeUInt32BE(highBits, this._blockSize - 8)\n this._block.writeUInt32BE(lowBits, this._blockSize - 4)\n }\n\n this._update(this._block)\n var hash = this._hash()\n\n return enc ? hash.toString(enc) : hash\n}\n\nHash.prototype._update = function () {\n throw new Error('_update must be implemented by subclass')\n}\n\nmodule.exports = Hash\n","var exports = module.exports = function SHA (algorithm) {\n algorithm = algorithm.toLowerCase()\n\n var Algorithm = exports[algorithm]\n if (!Algorithm) throw new Error(algorithm + ' is not supported (we accept pull requests)')\n\n return new Algorithm()\n}\n\nexports.sha = require('./sha')\nexports.sha1 = require('./sha1')\nexports.sha224 = require('./sha224')\nexports.sha256 = require('./sha256')\nexports.sha384 = require('./sha384')\nexports.sha512 = require('./sha512')\n","/*\n * A JavaScript implementation of the Secure Hash Algorithm, SHA-0, as defined\n * in FIPS PUB 180-1\n * This source code is derived from sha1.js of the same repository.\n * The difference between SHA-0 and SHA-1 is just a bitwise rotate left\n * operation was added.\n */\n\nvar inherits = require('inherits')\nvar Hash = require('./hash')\nvar Buffer = require('safe-buffer').Buffer\n\nvar K = [\n 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc | 0, 0xca62c1d6 | 0\n]\n\nvar W = new Array(80)\n\nfunction Sha () {\n this.init()\n this._w = W\n\n Hash.call(this, 64, 56)\n}\n\ninherits(Sha, Hash)\n\nSha.prototype.init = function () {\n this._a = 0x67452301\n this._b = 0xefcdab89\n this._c = 0x98badcfe\n this._d = 0x10325476\n this._e = 0xc3d2e1f0\n\n return this\n}\n\nfunction rotl5 (num) {\n return (num << 5) | (num >>> 27)\n}\n\nfunction rotl30 (num) {\n return (num << 30) | (num >>> 2)\n}\n\nfunction ft (s, b, c, d) {\n if (s === 0) return (b & c) | ((~b) & d)\n if (s === 2) return (b & c) | (b & d) | (c & d)\n return b ^ c ^ d\n}\n\nSha.prototype._update = function (M) {\n var W = this._w\n\n var a = this._a | 0\n var b = this._b | 0\n var c = this._c | 0\n var d = this._d | 0\n var e = this._e | 0\n\n for (var i = 0; i < 16; ++i) W[i] = M.readInt32BE(i * 4)\n for (; i < 80; ++i) W[i] = W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16]\n\n for (var j = 0; j < 80; ++j) {\n var s = ~~(j / 20)\n var t = (rotl5(a) + ft(s, b, c, d) + e + W[j] + K[s]) | 0\n\n e = d\n d = c\n c = rotl30(b)\n b = a\n a = t\n }\n\n this._a = (a + this._a) | 0\n this._b = (b + this._b) | 0\n this._c = (c + this._c) | 0\n this._d = (d + this._d) | 0\n this._e = (e + this._e) | 0\n}\n\nSha.prototype._hash = function () {\n var H = Buffer.allocUnsafe(20)\n\n H.writeInt32BE(this._a | 0, 0)\n H.writeInt32BE(this._b | 0, 4)\n H.writeInt32BE(this._c | 0, 8)\n H.writeInt32BE(this._d | 0, 12)\n H.writeInt32BE(this._e | 0, 16)\n\n return H\n}\n\nmodule.exports = Sha\n","/*\n * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined\n * in FIPS PUB 180-1\n * Version 2.1a Copyright Paul Johnston 2000 - 2002.\n * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet\n * Distributed under the BSD License\n * See http://pajhome.org.uk/crypt/md5 for details.\n */\n\nvar inherits = require('inherits')\nvar Hash = require('./hash')\nvar Buffer = require('safe-buffer').Buffer\n\nvar K = [\n 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc | 0, 0xca62c1d6 | 0\n]\n\nvar W = new Array(80)\n\nfunction Sha1 () {\n this.init()\n this._w = W\n\n Hash.call(this, 64, 56)\n}\n\ninherits(Sha1, Hash)\n\nSha1.prototype.init = function () {\n this._a = 0x67452301\n this._b = 0xefcdab89\n this._c = 0x98badcfe\n this._d = 0x10325476\n this._e = 0xc3d2e1f0\n\n return this\n}\n\nfunction rotl1 (num) {\n return (num << 1) | (num >>> 31)\n}\n\nfunction rotl5 (num) {\n return (num << 5) | (num >>> 27)\n}\n\nfunction rotl30 (num) {\n return (num << 30) | (num >>> 2)\n}\n\nfunction ft (s, b, c, d) {\n if (s === 0) return (b & c) | ((~b) & d)\n if (s === 2) return (b & c) | (b & d) | (c & d)\n return b ^ c ^ d\n}\n\nSha1.prototype._update = function (M) {\n var W = this._w\n\n var a = this._a | 0\n var b = this._b | 0\n var c = this._c | 0\n var d = this._d | 0\n var e = this._e | 0\n\n for (var i = 0; i < 16; ++i) W[i] = M.readInt32BE(i * 4)\n for (; i < 80; ++i) W[i] = rotl1(W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16])\n\n for (var j = 0; j < 80; ++j) {\n var s = ~~(j / 20)\n var t = (rotl5(a) + ft(s, b, c, d) + e + W[j] + K[s]) | 0\n\n e = d\n d = c\n c = rotl30(b)\n b = a\n a = t\n }\n\n this._a = (a + this._a) | 0\n this._b = (b + this._b) | 0\n this._c = (c + this._c) | 0\n this._d = (d + this._d) | 0\n this._e = (e + this._e) | 0\n}\n\nSha1.prototype._hash = function () {\n var H = Buffer.allocUnsafe(20)\n\n H.writeInt32BE(this._a | 0, 0)\n H.writeInt32BE(this._b | 0, 4)\n H.writeInt32BE(this._c | 0, 8)\n H.writeInt32BE(this._d | 0, 12)\n H.writeInt32BE(this._e | 0, 16)\n\n return H\n}\n\nmodule.exports = Sha1\n","/**\n * A JavaScript implementation of the Secure Hash Algorithm, SHA-256, as defined\n * in FIPS 180-2\n * Version 2.2-beta Copyright Angel Marin, Paul Johnston 2000 - 2009.\n * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet\n *\n */\n\nvar inherits = require('inherits')\nvar Sha256 = require('./sha256')\nvar Hash = require('./hash')\nvar Buffer = require('safe-buffer').Buffer\n\nvar W = new Array(64)\n\nfunction Sha224 () {\n this.init()\n\n this._w = W // new Array(64)\n\n Hash.call(this, 64, 56)\n}\n\ninherits(Sha224, Sha256)\n\nSha224.prototype.init = function () {\n this._a = 0xc1059ed8\n this._b = 0x367cd507\n this._c = 0x3070dd17\n this._d = 0xf70e5939\n this._e = 0xffc00b31\n this._f = 0x68581511\n this._g = 0x64f98fa7\n this._h = 0xbefa4fa4\n\n return this\n}\n\nSha224.prototype._hash = function () {\n var H = Buffer.allocUnsafe(28)\n\n H.writeInt32BE(this._a, 0)\n H.writeInt32BE(this._b, 4)\n H.writeInt32BE(this._c, 8)\n H.writeInt32BE(this._d, 12)\n H.writeInt32BE(this._e, 16)\n H.writeInt32BE(this._f, 20)\n H.writeInt32BE(this._g, 24)\n\n return H\n}\n\nmodule.exports = Sha224\n","/**\n * A JavaScript implementation of the Secure Hash Algorithm, SHA-256, as defined\n * in FIPS 180-2\n * Version 2.2-beta Copyright Angel Marin, Paul Johnston 2000 - 2009.\n * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet\n *\n */\n\nvar inherits = require('inherits')\nvar Hash = require('./hash')\nvar Buffer = require('safe-buffer').Buffer\n\nvar K = [\n 0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5,\n 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5,\n 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3,\n 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174,\n 0xE49B69C1, 0xEFBE4786, 0x0FC19DC6, 0x240CA1CC,\n 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA,\n 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7,\n 0xC6E00BF3, 0xD5A79147, 0x06CA6351, 0x14292967,\n 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13,\n 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85,\n 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3,\n 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070,\n 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5,\n 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3,\n 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208,\n 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2\n]\n\nvar W = new Array(64)\n\nfunction Sha256 () {\n this.init()\n\n this._w = W // new Array(64)\n\n Hash.call(this, 64, 56)\n}\n\ninherits(Sha256, Hash)\n\nSha256.prototype.init = function () {\n this._a = 0x6a09e667\n this._b = 0xbb67ae85\n this._c = 0x3c6ef372\n this._d = 0xa54ff53a\n this._e = 0x510e527f\n this._f = 0x9b05688c\n this._g = 0x1f83d9ab\n this._h = 0x5be0cd19\n\n return this\n}\n\nfunction ch (x, y, z) {\n return z ^ (x & (y ^ z))\n}\n\nfunction maj (x, y, z) {\n return (x & y) | (z & (x | y))\n}\n\nfunction sigma0 (x) {\n return (x >>> 2 | x << 30) ^ (x >>> 13 | x << 19) ^ (x >>> 22 | x << 10)\n}\n\nfunction sigma1 (x) {\n return (x >>> 6 | x << 26) ^ (x >>> 11 | x << 21) ^ (x >>> 25 | x << 7)\n}\n\nfunction gamma0 (x) {\n return (x >>> 7 | x << 25) ^ (x >>> 18 | x << 14) ^ (x >>> 3)\n}\n\nfunction gamma1 (x) {\n return (x >>> 17 | x << 15) ^ (x >>> 19 | x << 13) ^ (x >>> 10)\n}\n\nSha256.prototype._update = function (M) {\n var W = this._w\n\n var a = this._a | 0\n var b = this._b | 0\n var c = this._c | 0\n var d = this._d | 0\n var e = this._e | 0\n var f = this._f | 0\n var g = this._g | 0\n var h = this._h | 0\n\n for (var i = 0; i < 16; ++i) W[i] = M.readInt32BE(i * 4)\n for (; i < 64; ++i) W[i] = (gamma1(W[i - 2]) + W[i - 7] + gamma0(W[i - 15]) + W[i - 16]) | 0\n\n for (var j = 0; j < 64; ++j) {\n var T1 = (h + sigma1(e) + ch(e, f, g) + K[j] + W[j]) | 0\n var T2 = (sigma0(a) + maj(a, b, c)) | 0\n\n h = g\n g = f\n f = e\n e = (d + T1) | 0\n d = c\n c = b\n b = a\n a = (T1 + T2) | 0\n }\n\n this._a = (a + this._a) | 0\n this._b = (b + this._b) | 0\n this._c = (c + this._c) | 0\n this._d = (d + this._d) | 0\n this._e = (e + this._e) | 0\n this._f = (f + this._f) | 0\n this._g = (g + this._g) | 0\n this._h = (h + this._h) | 0\n}\n\nSha256.prototype._hash = function () {\n var H = Buffer.allocUnsafe(32)\n\n H.writeInt32BE(this._a, 0)\n H.writeInt32BE(this._b, 4)\n H.writeInt32BE(this._c, 8)\n H.writeInt32BE(this._d, 12)\n H.writeInt32BE(this._e, 16)\n H.writeInt32BE(this._f, 20)\n H.writeInt32BE(this._g, 24)\n H.writeInt32BE(this._h, 28)\n\n return H\n}\n\nmodule.exports = Sha256\n","var inherits = require('inherits')\nvar SHA512 = require('./sha512')\nvar Hash = require('./hash')\nvar Buffer = require('safe-buffer').Buffer\n\nvar W = new Array(160)\n\nfunction Sha384 () {\n this.init()\n this._w = W\n\n Hash.call(this, 128, 112)\n}\n\ninherits(Sha384, SHA512)\n\nSha384.prototype.init = function () {\n this._ah = 0xcbbb9d5d\n this._bh = 0x629a292a\n this._ch = 0x9159015a\n this._dh = 0x152fecd8\n this._eh = 0x67332667\n this._fh = 0x8eb44a87\n this._gh = 0xdb0c2e0d\n this._hh = 0x47b5481d\n\n this._al = 0xc1059ed8\n this._bl = 0x367cd507\n this._cl = 0x3070dd17\n this._dl = 0xf70e5939\n this._el = 0xffc00b31\n this._fl = 0x68581511\n this._gl = 0x64f98fa7\n this._hl = 0xbefa4fa4\n\n return this\n}\n\nSha384.prototype._hash = function () {\n var H = Buffer.allocUnsafe(48)\n\n function writeInt64BE (h, l, offset) {\n H.writeInt32BE(h, offset)\n H.writeInt32BE(l, offset + 4)\n }\n\n writeInt64BE(this._ah, this._al, 0)\n writeInt64BE(this._bh, this._bl, 8)\n writeInt64BE(this._ch, this._cl, 16)\n writeInt64BE(this._dh, this._dl, 24)\n writeInt64BE(this._eh, this._el, 32)\n writeInt64BE(this._fh, this._fl, 40)\n\n return H\n}\n\nmodule.exports = Sha384\n","var inherits = require('inherits')\nvar Hash = require('./hash')\nvar Buffer = require('safe-buffer').Buffer\n\nvar K = [\n 0x428a2f98, 0xd728ae22, 0x71374491, 0x23ef65cd,\n 0xb5c0fbcf, 0xec4d3b2f, 0xe9b5dba5, 0x8189dbbc,\n 0x3956c25b, 0xf348b538, 0x59f111f1, 0xb605d019,\n 0x923f82a4, 0xaf194f9b, 0xab1c5ed5, 0xda6d8118,\n 0xd807aa98, 0xa3030242, 0x12835b01, 0x45706fbe,\n 0x243185be, 0x4ee4b28c, 0x550c7dc3, 0xd5ffb4e2,\n 0x72be5d74, 0xf27b896f, 0x80deb1fe, 0x3b1696b1,\n 0x9bdc06a7, 0x25c71235, 0xc19bf174, 0xcf692694,\n 0xe49b69c1, 0x9ef14ad2, 0xefbe4786, 0x384f25e3,\n 0x0fc19dc6, 0x8b8cd5b5, 0x240ca1cc, 0x77ac9c65,\n 0x2de92c6f, 0x592b0275, 0x4a7484aa, 0x6ea6e483,\n 0x5cb0a9dc, 0xbd41fbd4, 0x76f988da, 0x831153b5,\n 0x983e5152, 0xee66dfab, 0xa831c66d, 0x2db43210,\n 0xb00327c8, 0x98fb213f, 0xbf597fc7, 0xbeef0ee4,\n 0xc6e00bf3, 0x3da88fc2, 0xd5a79147, 0x930aa725,\n 0x06ca6351, 0xe003826f, 0x14292967, 0x0a0e6e70,\n 0x27b70a85, 0x46d22ffc, 0x2e1b2138, 0x5c26c926,\n 0x4d2c6dfc, 0x5ac42aed, 0x53380d13, 0x9d95b3df,\n 0x650a7354, 0x8baf63de, 0x766a0abb, 0x3c77b2a8,\n 0x81c2c92e, 0x47edaee6, 0x92722c85, 0x1482353b,\n 0xa2bfe8a1, 0x4cf10364, 0xa81a664b, 0xbc423001,\n 0xc24b8b70, 0xd0f89791, 0xc76c51a3, 0x0654be30,\n 0xd192e819, 0xd6ef5218, 0xd6990624, 0x5565a910,\n 0xf40e3585, 0x5771202a, 0x106aa070, 0x32bbd1b8,\n 0x19a4c116, 0xb8d2d0c8, 0x1e376c08, 0x5141ab53,\n 0x2748774c, 0xdf8eeb99, 0x34b0bcb5, 0xe19b48a8,\n 0x391c0cb3, 0xc5c95a63, 0x4ed8aa4a, 0xe3418acb,\n 0x5b9cca4f, 0x7763e373, 0x682e6ff3, 0xd6b2b8a3,\n 0x748f82ee, 0x5defb2fc, 0x78a5636f, 0x43172f60,\n 0x84c87814, 0xa1f0ab72, 0x8cc70208, 0x1a6439ec,\n 0x90befffa, 0x23631e28, 0xa4506ceb, 0xde82bde9,\n 0xbef9a3f7, 0xb2c67915, 0xc67178f2, 0xe372532b,\n 0xca273ece, 0xea26619c, 0xd186b8c7, 0x21c0c207,\n 0xeada7dd6, 0xcde0eb1e, 0xf57d4f7f, 0xee6ed178,\n 0x06f067aa, 0x72176fba, 0x0a637dc5, 0xa2c898a6,\n 0x113f9804, 0xbef90dae, 0x1b710b35, 0x131c471b,\n 0x28db77f5, 0x23047d84, 0x32caab7b, 0x40c72493,\n 0x3c9ebe0a, 0x15c9bebc, 0x431d67c4, 0x9c100d4c,\n 0x4cc5d4be, 0xcb3e42b6, 0x597f299c, 0xfc657e2a,\n 0x5fcb6fab, 0x3ad6faec, 0x6c44198c, 0x4a475817\n]\n\nvar W = new Array(160)\n\nfunction Sha512 () {\n this.init()\n this._w = W\n\n Hash.call(this, 128, 112)\n}\n\ninherits(Sha512, Hash)\n\nSha512.prototype.init = function () {\n this._ah = 0x6a09e667\n this._bh = 0xbb67ae85\n this._ch = 0x3c6ef372\n this._dh = 0xa54ff53a\n this._eh = 0x510e527f\n this._fh = 0x9b05688c\n this._gh = 0x1f83d9ab\n this._hh = 0x5be0cd19\n\n this._al = 0xf3bcc908\n this._bl = 0x84caa73b\n this._cl = 0xfe94f82b\n this._dl = 0x5f1d36f1\n this._el = 0xade682d1\n this._fl = 0x2b3e6c1f\n this._gl = 0xfb41bd6b\n this._hl = 0x137e2179\n\n return this\n}\n\nfunction Ch (x, y, z) {\n return z ^ (x & (y ^ z))\n}\n\nfunction maj (x, y, z) {\n return (x & y) | (z & (x | y))\n}\n\nfunction sigma0 (x, xl) {\n return (x >>> 28 | xl << 4) ^ (xl >>> 2 | x << 30) ^ (xl >>> 7 | x << 25)\n}\n\nfunction sigma1 (x, xl) {\n return (x >>> 14 | xl << 18) ^ (x >>> 18 | xl << 14) ^ (xl >>> 9 | x << 23)\n}\n\nfunction Gamma0 (x, xl) {\n return (x >>> 1 | xl << 31) ^ (x >>> 8 | xl << 24) ^ (x >>> 7)\n}\n\nfunction Gamma0l (x, xl) {\n return (x >>> 1 | xl << 31) ^ (x >>> 8 | xl << 24) ^ (x >>> 7 | xl << 25)\n}\n\nfunction Gamma1 (x, xl) {\n return (x >>> 19 | xl << 13) ^ (xl >>> 29 | x << 3) ^ (x >>> 6)\n}\n\nfunction Gamma1l (x, xl) {\n return (x >>> 19 | xl << 13) ^ (xl >>> 29 | x << 3) ^ (x >>> 6 | xl << 26)\n}\n\nfunction getCarry (a, b) {\n return (a >>> 0) < (b >>> 0) ? 1 : 0\n}\n\nSha512.prototype._update = function (M) {\n var W = this._w\n\n var ah = this._ah | 0\n var bh = this._bh | 0\n var ch = this._ch | 0\n var dh = this._dh | 0\n var eh = this._eh | 0\n var fh = this._fh | 0\n var gh = this._gh | 0\n var hh = this._hh | 0\n\n var al = this._al | 0\n var bl = this._bl | 0\n var cl = this._cl | 0\n var dl = this._dl | 0\n var el = this._el | 0\n var fl = this._fl | 0\n var gl = this._gl | 0\n var hl = this._hl | 0\n\n for (var i = 0; i < 32; i += 2) {\n W[i] = M.readInt32BE(i * 4)\n W[i + 1] = M.readInt32BE(i * 4 + 4)\n }\n for (; i < 160; i += 2) {\n var xh = W[i - 15 * 2]\n var xl = W[i - 15 * 2 + 1]\n var gamma0 = Gamma0(xh, xl)\n var gamma0l = Gamma0l(xl, xh)\n\n xh = W[i - 2 * 2]\n xl = W[i - 2 * 2 + 1]\n var gamma1 = Gamma1(xh, xl)\n var gamma1l = Gamma1l(xl, xh)\n\n // W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16]\n var Wi7h = W[i - 7 * 2]\n var Wi7l = W[i - 7 * 2 + 1]\n\n var Wi16h = W[i - 16 * 2]\n var Wi16l = W[i - 16 * 2 + 1]\n\n var Wil = (gamma0l + Wi7l) | 0\n var Wih = (gamma0 + Wi7h + getCarry(Wil, gamma0l)) | 0\n Wil = (Wil + gamma1l) | 0\n Wih = (Wih + gamma1 + getCarry(Wil, gamma1l)) | 0\n Wil = (Wil + Wi16l) | 0\n Wih = (Wih + Wi16h + getCarry(Wil, Wi16l)) | 0\n\n W[i] = Wih\n W[i + 1] = Wil\n }\n\n for (var j = 0; j < 160; j += 2) {\n Wih = W[j]\n Wil = W[j + 1]\n\n var majh = maj(ah, bh, ch)\n var majl = maj(al, bl, cl)\n\n var sigma0h = sigma0(ah, al)\n var sigma0l = sigma0(al, ah)\n var sigma1h = sigma1(eh, el)\n var sigma1l = sigma1(el, eh)\n\n // t1 = h + sigma1 + ch + K[j] + W[j]\n var Kih = K[j]\n var Kil = K[j + 1]\n\n var chh = Ch(eh, fh, gh)\n var chl = Ch(el, fl, gl)\n\n var t1l = (hl + sigma1l) | 0\n var t1h = (hh + sigma1h + getCarry(t1l, hl)) | 0\n t1l = (t1l + chl) | 0\n t1h = (t1h + chh + getCarry(t1l, chl)) | 0\n t1l = (t1l + Kil) | 0\n t1h = (t1h + Kih + getCarry(t1l, Kil)) | 0\n t1l = (t1l + Wil) | 0\n t1h = (t1h + Wih + getCarry(t1l, Wil)) | 0\n\n // t2 = sigma0 + maj\n var t2l = (sigma0l + majl) | 0\n var t2h = (sigma0h + majh + getCarry(t2l, sigma0l)) | 0\n\n hh = gh\n hl = gl\n gh = fh\n gl = fl\n fh = eh\n fl = el\n el = (dl + t1l) | 0\n eh = (dh + t1h + getCarry(el, dl)) | 0\n dh = ch\n dl = cl\n ch = bh\n cl = bl\n bh = ah\n bl = al\n al = (t1l + t2l) | 0\n ah = (t1h + t2h + getCarry(al, t1l)) | 0\n }\n\n this._al = (this._al + al) | 0\n this._bl = (this._bl + bl) | 0\n this._cl = (this._cl + cl) | 0\n this._dl = (this._dl + dl) | 0\n this._el = (this._el + el) | 0\n this._fl = (this._fl + fl) | 0\n this._gl = (this._gl + gl) | 0\n this._hl = (this._hl + hl) | 0\n\n this._ah = (this._ah + ah + getCarry(this._al, al)) | 0\n this._bh = (this._bh + bh + getCarry(this._bl, bl)) | 0\n this._ch = (this._ch + ch + getCarry(this._cl, cl)) | 0\n this._dh = (this._dh + dh + getCarry(this._dl, dl)) | 0\n this._eh = (this._eh + eh + getCarry(this._el, el)) | 0\n this._fh = (this._fh + fh + getCarry(this._fl, fl)) | 0\n this._gh = (this._gh + gh + getCarry(this._gl, gl)) | 0\n this._hh = (this._hh + hh + getCarry(this._hl, hl)) | 0\n}\n\nSha512.prototype._hash = function () {\n var H = Buffer.allocUnsafe(64)\n\n function writeInt64BE (h, l, offset) {\n H.writeInt32BE(h, offset)\n H.writeInt32BE(l, offset + 4)\n }\n\n writeInt64BE(this._ah, this._al, 0)\n writeInt64BE(this._bh, this._bl, 8)\n writeInt64BE(this._ch, this._cl, 16)\n writeInt64BE(this._dh, this._dl, 24)\n writeInt64BE(this._eh, this._el, 32)\n writeInt64BE(this._fh, this._fl, 40)\n writeInt64BE(this._gh, this._gl, 48)\n writeInt64BE(this._hh, this._hl, 56)\n\n return H\n}\n\nmodule.exports = Sha512\n","// Copyright Joyent, Inc. and other Node contributors.\n//\n// Permission is hereby granted, free of charge, to any person obtaining a\n// copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to permit\n// persons to whom the Software is furnished to do so, subject to the\n// following conditions:\n//\n// The above copyright notice and this permission notice shall be included\n// in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\n// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN\n// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\n// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE\n// USE OR OTHER DEALINGS IN THE SOFTWARE.\n\nmodule.exports = Stream;\n\nvar EE = require('events').EventEmitter;\nvar inherits = require('inherits');\n\ninherits(Stream, EE);\nStream.Readable = require('readable-stream/lib/_stream_readable.js');\nStream.Writable = require('readable-stream/lib/_stream_writable.js');\nStream.Duplex = require('readable-stream/lib/_stream_duplex.js');\nStream.Transform = require('readable-stream/lib/_stream_transform.js');\nStream.PassThrough = require('readable-stream/lib/_stream_passthrough.js');\nStream.finished = require('readable-stream/lib/internal/streams/end-of-stream.js')\nStream.pipeline = require('readable-stream/lib/internal/streams/pipeline.js')\n\n// Backwards-compat with node 0.4.x\nStream.Stream = Stream;\n\n\n\n// old-style streams. Note that the pipe method (the only relevant\n// part of this class) is overridden in the Readable class.\n\nfunction Stream() {\n EE.call(this);\n}\n\nStream.prototype.pipe = function(dest, options) {\n var source = this;\n\n function ondata(chunk) {\n if (dest.writable) {\n if (false === dest.write(chunk) && source.pause) {\n source.pause();\n }\n }\n }\n\n source.on('data', ondata);\n\n function ondrain() {\n if (source.readable && source.resume) {\n source.resume();\n }\n }\n\n dest.on('drain', ondrain);\n\n // If the 'end' option is not supplied, dest.end() will be called when\n // source gets the 'end' or 'close' events. Only dest.end() once.\n if (!dest._isStdio && (!options || options.end !== false)) {\n source.on('end', onend);\n source.on('close', onclose);\n }\n\n var didOnEnd = false;\n function onend() {\n if (didOnEnd) return;\n didOnEnd = true;\n\n dest.end();\n }\n\n\n function onclose() {\n if (didOnEnd) return;\n didOnEnd = true;\n\n if (typeof dest.destroy === 'function') dest.destroy();\n }\n\n // don't leave dangling pipes when there are errors.\n function onerror(er) {\n cleanup();\n if (EE.listenerCount(this, 'error') === 0) {\n throw er; // Unhandled stream error in pipe.\n }\n }\n\n source.on('error', onerror);\n dest.on('error', onerror);\n\n // remove all the event listeners that were added.\n function cleanup() {\n source.removeListener('data', ondata);\n dest.removeListener('drain', ondrain);\n\n source.removeListener('end', onend);\n source.removeListener('close', onclose);\n\n source.removeListener('error', onerror);\n dest.removeListener('error', onerror);\n\n source.removeListener('end', cleanup);\n source.removeListener('close', cleanup);\n\n dest.removeListener('close', cleanup);\n }\n\n source.on('end', cleanup);\n source.on('close', cleanup);\n\n dest.on('close', cleanup);\n\n dest.emit('pipe', source);\n\n // Allow for unix-like usage: A.pipe(B).pipe(C)\n return dest;\n};\n","// Copyright Joyent, Inc. and other Node contributors.\n//\n// Permission is hereby granted, free of charge, to any person obtaining a\n// copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to permit\n// persons to whom the Software is furnished to do so, subject to the\n// following conditions:\n//\n// The above copyright notice and this permission notice shall be included\n// in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\n// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN\n// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\n// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE\n// USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n'use strict';\n\n/**/\n\nvar Buffer = require('safe-buffer').Buffer;\n/**/\n\nvar isEncoding = Buffer.isEncoding || function (encoding) {\n encoding = '' + encoding;\n switch (encoding && encoding.toLowerCase()) {\n case 'hex':case 'utf8':case 'utf-8':case 'ascii':case 'binary':case 'base64':case 'ucs2':case 'ucs-2':case 'utf16le':case 'utf-16le':case 'raw':\n return true;\n default:\n return false;\n }\n};\n\nfunction _normalizeEncoding(enc) {\n if (!enc) return 'utf8';\n var retried;\n while (true) {\n switch (enc) {\n case 'utf8':\n case 'utf-8':\n return 'utf8';\n case 'ucs2':\n case 'ucs-2':\n case 'utf16le':\n case 'utf-16le':\n return 'utf16le';\n case 'latin1':\n case 'binary':\n return 'latin1';\n case 'base64':\n case 'ascii':\n case 'hex':\n return enc;\n default:\n if (retried) return; // undefined\n enc = ('' + enc).toLowerCase();\n retried = true;\n }\n }\n};\n\n// Do not cache `Buffer.isEncoding` when checking encoding names as some\n// modules monkey-patch it to support additional encodings\nfunction normalizeEncoding(enc) {\n var nenc = _normalizeEncoding(enc);\n if (typeof nenc !== 'string' && (Buffer.isEncoding === isEncoding || !isEncoding(enc))) throw new Error('Unknown encoding: ' + enc);\n return nenc || enc;\n}\n\n// StringDecoder provides an interface for efficiently splitting a series of\n// buffers into a series of JS strings without breaking apart multi-byte\n// characters.\nexports.StringDecoder = StringDecoder;\nfunction StringDecoder(encoding) {\n this.encoding = normalizeEncoding(encoding);\n var nb;\n switch (this.encoding) {\n case 'utf16le':\n this.text = utf16Text;\n this.end = utf16End;\n nb = 4;\n break;\n case 'utf8':\n this.fillLast = utf8FillLast;\n nb = 4;\n break;\n case 'base64':\n this.text = base64Text;\n this.end = base64End;\n nb = 3;\n break;\n default:\n this.write = simpleWrite;\n this.end = simpleEnd;\n return;\n }\n this.lastNeed = 0;\n this.lastTotal = 0;\n this.lastChar = Buffer.allocUnsafe(nb);\n}\n\nStringDecoder.prototype.write = function (buf) {\n if (buf.length === 0) return '';\n var r;\n var i;\n if (this.lastNeed) {\n r = this.fillLast(buf);\n if (r === undefined) return '';\n i = this.lastNeed;\n this.lastNeed = 0;\n } else {\n i = 0;\n }\n if (i < buf.length) return r ? r + this.text(buf, i) : this.text(buf, i);\n return r || '';\n};\n\nStringDecoder.prototype.end = utf8End;\n\n// Returns only complete characters in a Buffer\nStringDecoder.prototype.text = utf8Text;\n\n// Attempts to complete a partial non-UTF-8 character using bytes from a Buffer\nStringDecoder.prototype.fillLast = function (buf) {\n if (this.lastNeed <= buf.length) {\n buf.copy(this.lastChar, this.lastTotal - this.lastNeed, 0, this.lastNeed);\n return this.lastChar.toString(this.encoding, 0, this.lastTotal);\n }\n buf.copy(this.lastChar, this.lastTotal - this.lastNeed, 0, buf.length);\n this.lastNeed -= buf.length;\n};\n\n// Checks the type of a UTF-8 byte, whether it's ASCII, a leading byte, or a\n// continuation byte. If an invalid byte is detected, -2 is returned.\nfunction utf8CheckByte(byte) {\n if (byte <= 0x7F) return 0;else if (byte >> 5 === 0x06) return 2;else if (byte >> 4 === 0x0E) return 3;else if (byte >> 3 === 0x1E) return 4;\n return byte >> 6 === 0x02 ? -1 : -2;\n}\n\n// Checks at most 3 bytes at the end of a Buffer in order to detect an\n// incomplete multi-byte UTF-8 character. The total number of bytes (2, 3, or 4)\n// needed to complete the UTF-8 character (if applicable) are returned.\nfunction utf8CheckIncomplete(self, buf, i) {\n var j = buf.length - 1;\n if (j < i) return 0;\n var nb = utf8CheckByte(buf[j]);\n if (nb >= 0) {\n if (nb > 0) self.lastNeed = nb - 1;\n return nb;\n }\n if (--j < i || nb === -2) return 0;\n nb = utf8CheckByte(buf[j]);\n if (nb >= 0) {\n if (nb > 0) self.lastNeed = nb - 2;\n return nb;\n }\n if (--j < i || nb === -2) return 0;\n nb = utf8CheckByte(buf[j]);\n if (nb >= 0) {\n if (nb > 0) {\n if (nb === 2) nb = 0;else self.lastNeed = nb - 3;\n }\n return nb;\n }\n return 0;\n}\n\n// Validates as many continuation bytes for a multi-byte UTF-8 character as\n// needed or are available. If we see a non-continuation byte where we expect\n// one, we \"replace\" the validated continuation bytes we've seen so far with\n// a single UTF-8 replacement character ('\\ufffd'), to match v8's UTF-8 decoding\n// behavior. The continuation byte check is included three times in the case\n// where all of the continuation bytes for a character exist in the same buffer.\n// It is also done this way as a slight performance increase instead of using a\n// loop.\nfunction utf8CheckExtraBytes(self, buf, p) {\n if ((buf[0] & 0xC0) !== 0x80) {\n self.lastNeed = 0;\n return '\\ufffd';\n }\n if (self.lastNeed > 1 && buf.length > 1) {\n if ((buf[1] & 0xC0) !== 0x80) {\n self.lastNeed = 1;\n return '\\ufffd';\n }\n if (self.lastNeed > 2 && buf.length > 2) {\n if ((buf[2] & 0xC0) !== 0x80) {\n self.lastNeed = 2;\n return '\\ufffd';\n }\n }\n }\n}\n\n// Attempts to complete a multi-byte UTF-8 character using bytes from a Buffer.\nfunction utf8FillLast(buf) {\n var p = this.lastTotal - this.lastNeed;\n var r = utf8CheckExtraBytes(this, buf, p);\n if (r !== undefined) return r;\n if (this.lastNeed <= buf.length) {\n buf.copy(this.lastChar, p, 0, this.lastNeed);\n return this.lastChar.toString(this.encoding, 0, this.lastTotal);\n }\n buf.copy(this.lastChar, p, 0, buf.length);\n this.lastNeed -= buf.length;\n}\n\n// Returns all complete UTF-8 characters in a Buffer. If the Buffer ended on a\n// partial character, the character's bytes are buffered until the required\n// number of bytes are available.\nfunction utf8Text(buf, i) {\n var total = utf8CheckIncomplete(this, buf, i);\n if (!this.lastNeed) return buf.toString('utf8', i);\n this.lastTotal = total;\n var end = buf.length - (total - this.lastNeed);\n buf.copy(this.lastChar, 0, end);\n return buf.toString('utf8', i, end);\n}\n\n// For UTF-8, a replacement character is added when ending on a partial\n// character.\nfunction utf8End(buf) {\n var r = buf && buf.length ? this.write(buf) : '';\n if (this.lastNeed) return r + '\\ufffd';\n return r;\n}\n\n// UTF-16LE typically needs two bytes per character, but even if we have an even\n// number of bytes available, we need to check if we end on a leading/high\n// surrogate. In that case, we need to wait for the next two bytes in order to\n// decode the last character properly.\nfunction utf16Text(buf, i) {\n if ((buf.length - i) % 2 === 0) {\n var r = buf.toString('utf16le', i);\n if (r) {\n var c = r.charCodeAt(r.length - 1);\n if (c >= 0xD800 && c <= 0xDBFF) {\n this.lastNeed = 2;\n this.lastTotal = 4;\n this.lastChar[0] = buf[buf.length - 2];\n this.lastChar[1] = buf[buf.length - 1];\n return r.slice(0, -1);\n }\n }\n return r;\n }\n this.lastNeed = 1;\n this.lastTotal = 2;\n this.lastChar[0] = buf[buf.length - 1];\n return buf.toString('utf16le', i, buf.length - 1);\n}\n\n// For UTF-16LE we do not explicitly append special replacement characters if we\n// end on a partial character, we simply let v8 handle that.\nfunction utf16End(buf) {\n var r = buf && buf.length ? this.write(buf) : '';\n if (this.lastNeed) {\n var end = this.lastTotal - this.lastNeed;\n return r + this.lastChar.toString('utf16le', 0, end);\n }\n return r;\n}\n\nfunction base64Text(buf, i) {\n var n = (buf.length - i) % 3;\n if (n === 0) return buf.toString('base64', i);\n this.lastNeed = 3 - n;\n this.lastTotal = 3;\n if (n === 1) {\n this.lastChar[0] = buf[buf.length - 1];\n } else {\n this.lastChar[0] = buf[buf.length - 2];\n this.lastChar[1] = buf[buf.length - 1];\n }\n return buf.toString('base64', i, buf.length - n);\n}\n\nfunction base64End(buf) {\n var r = buf && buf.length ? this.write(buf) : '';\n if (this.lastNeed) return r + this.lastChar.toString('base64', 0, 3 - this.lastNeed);\n return r;\n}\n\n// Pass bytes on through for single-byte encodings (e.g. ascii, latin1, hex)\nfunction simpleWrite(buf) {\n return buf.toString(this.encoding);\n}\n\nfunction simpleEnd(buf) {\n return buf && buf.length ? this.write(buf) : '';\n}","\n/**\n * Module exports.\n */\n\nmodule.exports = deprecate;\n\n/**\n * Mark that a method should not be used.\n * Returns a modified function which warns once by default.\n *\n * If `localStorage.noDeprecation = true` is set, then it is a no-op.\n *\n * If `localStorage.throwDeprecation = true` is set, then deprecated functions\n * will throw an Error when invoked.\n *\n * If `localStorage.traceDeprecation = true` is set, then deprecated functions\n * will invoke `console.trace()` instead of `console.error()`.\n *\n * @param {Function} fn - the function to deprecate\n * @param {String} msg - the string to print to the console when `fn` is invoked\n * @returns {Function} a new \"deprecated\" version of `fn`\n * @api public\n */\n\nfunction deprecate (fn, msg) {\n if (config('noDeprecation')) {\n return fn;\n }\n\n var warned = false;\n function deprecated() {\n if (!warned) {\n if (config('throwDeprecation')) {\n throw new Error(msg);\n } else if (config('traceDeprecation')) {\n console.trace(msg);\n } else {\n console.warn(msg);\n }\n warned = true;\n }\n return fn.apply(this, arguments);\n }\n\n return deprecated;\n}\n\n/**\n * Checks `localStorage` for boolean values for the given `name`.\n *\n * @param {String} name\n * @returns {Boolean}\n * @api private\n */\n\nfunction config (name) {\n // accessing global.localStorage can trigger a DOMException in sandboxed iframes\n try {\n if (!global.localStorage) return false;\n } catch (_) {\n return false;\n }\n var val = global.localStorage[name];\n if (null == val) return false;\n return String(val).toLowerCase() === 'true';\n}\n","\nvar XML_CHARACTER_MAP = {\n '&': '&',\n '\"': '"',\n \"'\": ''',\n '<': '<',\n '>': '>'\n};\n\nfunction escapeForXML(string) {\n return string && string.replace\n ? string.replace(/([&\"<>'])/g, function(str, item) {\n return XML_CHARACTER_MAP[item];\n })\n : string;\n}\n\nmodule.exports = escapeForXML;\n","var escapeForXML = require('./escapeForXML');\nvar Stream = require('stream').Stream;\n\nvar DEFAULT_INDENT = ' ';\n\nfunction xml(input, options) {\n\n if (typeof options !== 'object') {\n options = {\n indent: options\n };\n }\n\n var stream = options.stream ? new Stream() : null,\n output = \"\",\n interrupted = false,\n indent = !options.indent ? ''\n : options.indent === true ? DEFAULT_INDENT\n : options.indent,\n instant = true;\n\n\n function delay (func) {\n if (!instant) {\n func();\n } else {\n process.nextTick(func);\n }\n }\n\n function append (interrupt, out) {\n if (out !== undefined) {\n output += out;\n }\n if (interrupt && !interrupted) {\n stream = stream || new Stream();\n interrupted = true;\n }\n if (interrupt && interrupted) {\n var data = output;\n delay(function () { stream.emit('data', data) });\n output = \"\";\n }\n }\n\n function add (value, last) {\n format(append, resolve(value, indent, indent ? 1 : 0), last);\n }\n\n function end() {\n if (stream) {\n var data = output;\n delay(function () {\n stream.emit('data', data);\n stream.emit('end');\n stream.readable = false;\n stream.emit('close');\n });\n }\n }\n\n function addXmlDeclaration(declaration) {\n var encoding = declaration.encoding || 'UTF-8',\n attr = { version: '1.0', encoding: encoding };\n\n if (declaration.standalone) {\n attr.standalone = declaration.standalone\n }\n\n add({'?xml': { _attr: attr } });\n output = output.replace('/>', '?>');\n }\n\n // disable delay delayed\n delay(function () { instant = false });\n\n if (options.declaration) {\n addXmlDeclaration(options.declaration);\n }\n\n if (input && input.forEach) {\n input.forEach(function (value, i) {\n var last;\n if (i + 1 === input.length)\n last = end;\n add(value, last);\n });\n } else {\n add(input, end);\n }\n\n if (stream) {\n stream.readable = true;\n return stream;\n }\n return output;\n}\n\nfunction element (/*input, …*/) {\n var input = Array.prototype.slice.call(arguments),\n self = {\n _elem: resolve(input)\n };\n\n self.push = function (input) {\n if (!this.append) {\n throw new Error(\"not assigned to a parent!\");\n }\n var that = this;\n var indent = this._elem.indent;\n format(this.append, resolve(\n input, indent, this._elem.icount + (indent ? 1 : 0)),\n function () { that.append(true) });\n };\n\n self.close = function (input) {\n if (input !== undefined) {\n this.push(input);\n }\n if (this.end) {\n this.end();\n }\n };\n\n return self;\n}\n\nfunction create_indent(character, count) {\n return (new Array(count || 0).join(character || ''))\n}\n\nfunction resolve(data, indent, indent_count) {\n indent_count = indent_count || 0;\n var indent_spaces = create_indent(indent, indent_count);\n var name;\n var values = data;\n var interrupt = false;\n\n if (typeof data === 'object') {\n var keys = Object.keys(data);\n name = keys[0];\n values = data[name];\n\n if (values && values._elem) {\n values._elem.name = name;\n values._elem.icount = indent_count;\n values._elem.indent = indent;\n values._elem.indents = indent_spaces;\n values._elem.interrupt = values;\n return values._elem;\n }\n }\n\n var attributes = [],\n content = [];\n\n var isStringContent;\n\n function get_attributes(obj){\n var keys = Object.keys(obj);\n keys.forEach(function(key){\n attributes.push(attribute(key, obj[key]));\n });\n }\n\n switch(typeof values) {\n case 'object':\n if (values === null) break;\n\n if (values._attr) {\n get_attributes(values._attr);\n }\n\n if (values._cdata) {\n content.push(\n ('/g, ']]]]>') + ']]>'\n );\n }\n\n if (values.forEach) {\n isStringContent = false;\n content.push('');\n values.forEach(function(value) {\n if (typeof value == 'object') {\n var _name = Object.keys(value)[0];\n\n if (_name == '_attr') {\n get_attributes(value._attr);\n } else {\n content.push(resolve(\n value, indent, indent_count + 1));\n }\n } else {\n //string\n content.pop();\n isStringContent=true;\n content.push(escapeForXML(value));\n }\n\n });\n if (!isStringContent) {\n content.push('');\n }\n }\n break;\n\n default:\n //string\n content.push(escapeForXML(values));\n\n }\n\n return {\n name: name,\n interrupt: interrupt,\n attributes: attributes,\n content: content,\n icount: indent_count,\n indents: indent_spaces,\n indent: indent\n };\n}\n\nfunction format(append, elem, end) {\n\n if (typeof elem != 'object') {\n return append(false, elem);\n }\n\n var len = elem.interrupt ? 1 : elem.content.length;\n\n function proceed () {\n while (elem.content.length) {\n var value = elem.content.shift();\n\n if (value === undefined) continue;\n if (interrupt(value)) return;\n\n format(append, value);\n }\n\n append(false, (len > 1 ? elem.indents : '')\n + (elem.name ? '' : '')\n + (elem.indent && !end ? '\\n' : ''));\n\n if (end) {\n end();\n }\n }\n\n function interrupt(value) {\n if (value.interrupt) {\n value.interrupt.append = append;\n value.interrupt.end = proceed;\n value.interrupt = false;\n append(true);\n return true;\n }\n return false;\n }\n\n append(false, elem.indents\n + (elem.name ? '<' + elem.name : '')\n + (elem.attributes.length ? ' ' + elem.attributes.join(' ') : '')\n + (len ? (elem.name ? '>' : '') : (elem.name ? '/>' : ''))\n + (elem.indent && len > 1 ? '\\n' : ''));\n\n if (!len) {\n return append(false, elem.indent ? '\\n' : '');\n }\n\n if (!interrupt(elem)) {\n proceed();\n }\n}\n\nfunction attribute(key, value) {\n return key + '=' + '\"' + escapeForXML(value) + '\"';\n}\n\nmodule.exports = xml;\nmodule.exports.element = module.exports.Element = element;\n","var map = {\n\t\"./all.js\": 5308,\n\t\"./auth/actions.js\": 5812,\n\t\"./auth/configs-extensions/wrap-actions.js\": 3779,\n\t\"./auth/index.js\": 3705,\n\t\"./auth/reducers.js\": 3962,\n\t\"./auth/selectors.js\": 35,\n\t\"./auth/spec-extensions/wrap-actions.js\": 489,\n\t\"./auth/wrap-actions.js\": 2849,\n\t\"./configs/actions.js\": 714,\n\t\"./configs/helpers.js\": 2256,\n\t\"./configs/index.js\": 6709,\n\t\"./configs/reducers.js\": 7743,\n\t\"./configs/selectors.js\": 9018,\n\t\"./configs/spec-actions.js\": 2698,\n\t\"./deep-linking/helpers.js\": 1970,\n\t\"./deep-linking/index.js\": 4980,\n\t\"./deep-linking/layout.js\": 5858,\n\t\"./deep-linking/operation-tag-wrapper.jsx\": 4584,\n\t\"./deep-linking/operation-wrapper.jsx\": 877,\n\t\"./download-url.js\": 8011,\n\t\"./err/actions.js\": 4966,\n\t\"./err/error-transformers/hook.js\": 6808,\n\t\"./err/error-transformers/transformers/not-of-type.js\": 2392,\n\t\"./err/error-transformers/transformers/parameter-oneof.js\": 1835,\n\t\"./err/index.js\": 7793,\n\t\"./err/reducers.js\": 3527,\n\t\"./err/selectors.js\": 7667,\n\t\"./filter/index.js\": 9978,\n\t\"./filter/opsFilter.js\": 4309,\n\t\"./json-schema-2020-12/components/Accordion/Accordion.jsx\": 7349,\n\t\"./json-schema-2020-12/components/ExpandDeepButton/ExpandDeepButton.jsx\": 6867,\n\t\"./json-schema-2020-12/components/JSONSchema/JSONSchema.jsx\": 2675,\n\t\"./json-schema-2020-12/components/icons/ChevronRight.jsx\": 2260,\n\t\"./json-schema-2020-12/components/keywords/$anchor.jsx\": 4922,\n\t\"./json-schema-2020-12/components/keywords/$comment.jsx\": 4685,\n\t\"./json-schema-2020-12/components/keywords/$defs.jsx\": 6418,\n\t\"./json-schema-2020-12/components/keywords/$dynamicAnchor.jsx\": 1338,\n\t\"./json-schema-2020-12/components/keywords/$dynamicRef.jsx\": 7655,\n\t\"./json-schema-2020-12/components/keywords/$id.jsx\": 3460,\n\t\"./json-schema-2020-12/components/keywords/$ref.jsx\": 2348,\n\t\"./json-schema-2020-12/components/keywords/$schema.jsx\": 9359,\n\t\"./json-schema-2020-12/components/keywords/$vocabulary/$vocabulary.jsx\": 7568,\n\t\"./json-schema-2020-12/components/keywords/AdditionalProperties.jsx\": 5253,\n\t\"./json-schema-2020-12/components/keywords/AllOf.jsx\": 6457,\n\t\"./json-schema-2020-12/components/keywords/AnyOf.jsx\": 8776,\n\t\"./json-schema-2020-12/components/keywords/Const.jsx\": 7308,\n\t\"./json-schema-2020-12/components/keywords/Constraint/Constraint.jsx\": 9956,\n\t\"./json-schema-2020-12/components/keywords/Contains.jsx\": 8993,\n\t\"./json-schema-2020-12/components/keywords/ContentSchema.jsx\": 3484,\n\t\"./json-schema-2020-12/components/keywords/Default.jsx\": 5148,\n\t\"./json-schema-2020-12/components/keywords/DependentRequired/DependentRequired.jsx\": 4539,\n\t\"./json-schema-2020-12/components/keywords/DependentSchemas.jsx\": 6076,\n\t\"./json-schema-2020-12/components/keywords/Deprecated.jsx\": 6661,\n\t\"./json-schema-2020-12/components/keywords/Description/Description.jsx\": 9446,\n\t\"./json-schema-2020-12/components/keywords/Else.jsx\": 7207,\n\t\"./json-schema-2020-12/components/keywords/Enum/Enum.jsx\": 1805,\n\t\"./json-schema-2020-12/components/keywords/If.jsx\": 487,\n\t\"./json-schema-2020-12/components/keywords/Items.jsx\": 9206,\n\t\"./json-schema-2020-12/components/keywords/Not.jsx\": 5174,\n\t\"./json-schema-2020-12/components/keywords/OneOf.jsx\": 3834,\n\t\"./json-schema-2020-12/components/keywords/PatternProperties/PatternProperties.jsx\": 6746,\n\t\"./json-schema-2020-12/components/keywords/PrefixItems.jsx\": 3971,\n\t\"./json-schema-2020-12/components/keywords/Properties/Properties.jsx\": 5472,\n\t\"./json-schema-2020-12/components/keywords/PropertyNames.jsx\": 2338,\n\t\"./json-schema-2020-12/components/keywords/ReadOnly.jsx\": 6456,\n\t\"./json-schema-2020-12/components/keywords/Then.jsx\": 7401,\n\t\"./json-schema-2020-12/components/keywords/Title/Title.jsx\": 8137,\n\t\"./json-schema-2020-12/components/keywords/Type.jsx\": 2285,\n\t\"./json-schema-2020-12/components/keywords/UnevaluatedItems.jsx\": 5828,\n\t\"./json-schema-2020-12/components/keywords/UnevaluatedProperties.jsx\": 6907,\n\t\"./json-schema-2020-12/components/keywords/WriteOnly.jsx\": 5789,\n\t\"./json-schema-2020-12/context.js\": 9006,\n\t\"./json-schema-2020-12/fn.js\": 4121,\n\t\"./json-schema-2020-12/hoc.jsx\": 5077,\n\t\"./json-schema-2020-12/hooks.js\": 2603,\n\t\"./json-schema-2020-12/index.js\": 7139,\n\t\"./json-schema-2020-12/prop-types.js\": 6648,\n\t\"./json-schema-2020-12/samples-extensions/fn/api/encoderAPI.js\": 9507,\n\t\"./json-schema-2020-12/samples-extensions/fn/api/formatAPI.js\": 2906,\n\t\"./json-schema-2020-12/samples-extensions/fn/api/mediaTypeAPI.js\": 537,\n\t\"./json-schema-2020-12/samples-extensions/fn/class/EncoderRegistry.js\": 674,\n\t\"./json-schema-2020-12/samples-extensions/fn/class/MediaTypeRegistry.js\": 3782,\n\t\"./json-schema-2020-12/samples-extensions/fn/class/Registry.js\": 4215,\n\t\"./json-schema-2020-12/samples-extensions/fn/core/constants.js\": 8338,\n\t\"./json-schema-2020-12/samples-extensions/fn/core/example.js\": 3783,\n\t\"./json-schema-2020-12/samples-extensions/fn/core/merge.js\": 7078,\n\t\"./json-schema-2020-12/samples-extensions/fn/core/predicates.js\": 3084,\n\t\"./json-schema-2020-12/samples-extensions/fn/core/random.js\": 5202,\n\t\"./json-schema-2020-12/samples-extensions/fn/core/type.js\": 6276,\n\t\"./json-schema-2020-12/samples-extensions/fn/core/utils.js\": 9346,\n\t\"./json-schema-2020-12/samples-extensions/fn/encoders/7bit.js\": 1433,\n\t\"./json-schema-2020-12/samples-extensions/fn/encoders/8bit.js\": 8509,\n\t\"./json-schema-2020-12/samples-extensions/fn/encoders/base16.js\": 5709,\n\t\"./json-schema-2020-12/samples-extensions/fn/encoders/base32.js\": 4180,\n\t\"./json-schema-2020-12/samples-extensions/fn/encoders/base64.js\": 1967,\n\t\"./json-schema-2020-12/samples-extensions/fn/encoders/binary.js\": 4366,\n\t\"./json-schema-2020-12/samples-extensions/fn/encoders/quoted-printable.js\": 5037,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/date-time.js\": 4045,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/date.js\": 1456,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/double.js\": 560,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/duration.js\": 4299,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/email.js\": 3981,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/float.js\": 1890,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/hostname.js\": 9375,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/idn-email.js\": 4518,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/idn-hostname.js\": 273,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/int32.js\": 7864,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/int64.js\": 1726,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/ipv4.js\": 8793,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/ipv6.js\": 8269,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/iri-reference.js\": 5693,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/iri.js\": 3080,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/json-pointer.js\": 7856,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/media-types/application.js\": 5652,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/media-types/audio.js\": 4342,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/media-types/image.js\": 6724,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/media-types/text.js\": 5378,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/media-types/video.js\": 2974,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/password.js\": 3393,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/regex.js\": 4335,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/relative-json-pointer.js\": 375,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/time.js\": 5243,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/uri-reference.js\": 4692,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/uri-template.js\": 3829,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/uri.js\": 2978,\n\t\"./json-schema-2020-12/samples-extensions/fn/generators/uuid.js\": 8859,\n\t\"./json-schema-2020-12/samples-extensions/fn/index.js\": 8591,\n\t\"./json-schema-2020-12/samples-extensions/fn/main.js\": 4277,\n\t\"./json-schema-2020-12/samples-extensions/fn/types/array.js\": 8262,\n\t\"./json-schema-2020-12/samples-extensions/fn/types/boolean.js\": 4108,\n\t\"./json-schema-2020-12/samples-extensions/fn/types/index.js\": 3273,\n\t\"./json-schema-2020-12/samples-extensions/fn/types/integer.js\": 8864,\n\t\"./json-schema-2020-12/samples-extensions/fn/types/null.js\": 853,\n\t\"./json-schema-2020-12/samples-extensions/fn/types/number.js\": 7742,\n\t\"./json-schema-2020-12/samples-extensions/fn/types/object.js\": 6852,\n\t\"./json-schema-2020-12/samples-extensions/fn/types/string.js\": 4522,\n\t\"./layout/actions.js\": 5474,\n\t\"./layout/index.js\": 6821,\n\t\"./layout/reducers.js\": 5672,\n\t\"./layout/selectors.js\": 4400,\n\t\"./layout/spec-extensions/wrap-selector.js\": 8989,\n\t\"./logs/index.js\": 9150,\n\t\"./oas3/actions.js\": 7002,\n\t\"./oas3/auth-extensions/wrap-selectors.js\": 3723,\n\t\"./oas3/components/callbacks.jsx\": 3427,\n\t\"./oas3/components/http-auth.jsx\": 6775,\n\t\"./oas3/components/index.js\": 6467,\n\t\"./oas3/components/operation-link.jsx\": 5757,\n\t\"./oas3/components/operation-servers.jsx\": 6796,\n\t\"./oas3/components/request-body-editor.jsx\": 5327,\n\t\"./oas3/components/request-body.jsx\": 2458,\n\t\"./oas3/components/servers-container.jsx\": 9928,\n\t\"./oas3/components/servers.jsx\": 6617,\n\t\"./oas3/helpers.jsx\": 7779,\n\t\"./oas3/index.js\": 7451,\n\t\"./oas3/reducers.js\": 2109,\n\t\"./oas3/selectors.js\": 5065,\n\t\"./oas3/spec-extensions/selectors.js\": 1741,\n\t\"./oas3/spec-extensions/wrap-selectors.js\": 2044,\n\t\"./oas3/wrap-components/auth-item.jsx\": 356,\n\t\"./oas3/wrap-components/index.js\": 7761,\n\t\"./oas3/wrap-components/json-schema-string.jsx\": 287,\n\t\"./oas3/wrap-components/markdown.jsx\": 2460,\n\t\"./oas3/wrap-components/model.jsx\": 3499,\n\t\"./oas3/wrap-components/online-validator-badge.js\": 58,\n\t\"./oas3/wrap-components/version-stamp.jsx\": 9487,\n\t\"./oas31/after-load.js\": 7754,\n\t\"./oas31/components/contact.jsx\": 9503,\n\t\"./oas31/components/info.jsx\": 6133,\n\t\"./oas31/components/json-schema-dialect.jsx\": 2562,\n\t\"./oas31/components/license.jsx\": 1876,\n\t\"./oas31/components/model/model.jsx\": 2718,\n\t\"./oas31/components/models/models.jsx\": 263,\n\t\"./oas31/components/version-pragma-filter.jsx\": 3429,\n\t\"./oas31/components/webhooks.jsx\": 9508,\n\t\"./oas31/fn.js\": 4380,\n\t\"./oas31/index.js\": 9806,\n\t\"./oas31/json-schema-2020-12-extensions/components/keywords/Description.jsx\": 5989,\n\t\"./oas31/json-schema-2020-12-extensions/components/keywords/Discriminator/Discriminator.jsx\": 9525,\n\t\"./oas31/json-schema-2020-12-extensions/components/keywords/Discriminator/DiscriminatorMapping.jsx\": 7749,\n\t\"./oas31/json-schema-2020-12-extensions/components/keywords/Example.jsx\": 9450,\n\t\"./oas31/json-schema-2020-12-extensions/components/keywords/ExternalDocs.jsx\": 5324,\n\t\"./oas31/json-schema-2020-12-extensions/components/keywords/Properties.jsx\": 9023,\n\t\"./oas31/json-schema-2020-12-extensions/components/keywords/Xml.jsx\": 3995,\n\t\"./oas31/json-schema-2020-12-extensions/fn.js\": 5800,\n\t\"./oas31/json-schema-2020-12-extensions/wrap-components/keywords/Default.jsx\": 4951,\n\t\"./oas31/json-schema-2020-12-extensions/wrap-components/keywords/Description.jsx\": 809,\n\t\"./oas31/json-schema-2020-12-extensions/wrap-components/keywords/Properties.jsx\": 7536,\n\t\"./oas31/selectors.js\": 4280,\n\t\"./oas31/spec-extensions/selectors.js\": 9305,\n\t\"./oas31/spec-extensions/wrap-selectors.js\": 2884,\n\t\"./oas31/wrap-components/contact.jsx\": 7423,\n\t\"./oas31/wrap-components/info.jsx\": 284,\n\t\"./oas31/wrap-components/license.jsx\": 6608,\n\t\"./oas31/wrap-components/model.jsx\": 7042,\n\t\"./oas31/wrap-components/models.jsx\": 2914,\n\t\"./oas31/wrap-components/version-pragma-filter.jsx\": 1434,\n\t\"./oas31/wrap-components/version-stamp.jsx\": 1122,\n\t\"./on-complete/index.js\": 8560,\n\t\"./request-snippets/fn.js\": 4624,\n\t\"./request-snippets/index.js\": 6575,\n\t\"./request-snippets/request-snippets.jsx\": 4206,\n\t\"./request-snippets/selectors.js\": 4669,\n\t\"./safe-render/components/error-boundary.jsx\": 6195,\n\t\"./safe-render/components/fallback.jsx\": 9403,\n\t\"./safe-render/fn.jsx\": 6189,\n\t\"./safe-render/index.js\": 8102,\n\t\"./samples/fn/get-json-sample-schema.js\": 2846,\n\t\"./samples/fn/get-sample-schema.js\": 6132,\n\t\"./samples/fn/get-xml-sample-schema.js\": 1169,\n\t\"./samples/fn/get-yaml-sample-schema.js\": 9431,\n\t\"./samples/fn/index.js\": 9812,\n\t\"./samples/index.js\": 8883,\n\t\"./spec/actions.js\": 7612,\n\t\"./spec/index.js\": 7038,\n\t\"./spec/reducers.js\": 32,\n\t\"./spec/selectors.js\": 3881,\n\t\"./spec/wrap-actions.js\": 7508,\n\t\"./swagger-js/configs-wrap-actions.js\": 4852,\n\t\"./swagger-js/index.js\": 9430,\n\t\"./util/index.js\": 8525,\n\t\"./view/fn.js\": 8347,\n\t\"./view/index.js\": 3420,\n\t\"./view/root-injects.jsx\": 3934,\n\t\"core/plugins/all.js\": 5308,\n\t\"core/plugins/auth/actions.js\": 5812,\n\t\"core/plugins/auth/configs-extensions/wrap-actions.js\": 3779,\n\t\"core/plugins/auth/index.js\": 3705,\n\t\"core/plugins/auth/reducers.js\": 3962,\n\t\"core/plugins/auth/selectors.js\": 35,\n\t\"core/plugins/auth/spec-extensions/wrap-actions.js\": 489,\n\t\"core/plugins/auth/wrap-actions.js\": 2849,\n\t\"core/plugins/configs/actions.js\": 714,\n\t\"core/plugins/configs/helpers.js\": 2256,\n\t\"core/plugins/configs/index.js\": 6709,\n\t\"core/plugins/configs/reducers.js\": 7743,\n\t\"core/plugins/configs/selectors.js\": 9018,\n\t\"core/plugins/configs/spec-actions.js\": 2698,\n\t\"core/plugins/deep-linking/helpers.js\": 1970,\n\t\"core/plugins/deep-linking/index.js\": 4980,\n\t\"core/plugins/deep-linking/layout.js\": 5858,\n\t\"core/plugins/deep-linking/operation-tag-wrapper.jsx\": 4584,\n\t\"core/plugins/deep-linking/operation-wrapper.jsx\": 877,\n\t\"core/plugins/download-url.js\": 8011,\n\t\"core/plugins/err/actions.js\": 4966,\n\t\"core/plugins/err/error-transformers/hook.js\": 6808,\n\t\"core/plugins/err/error-transformers/transformers/not-of-type.js\": 2392,\n\t\"core/plugins/err/error-transformers/transformers/parameter-oneof.js\": 1835,\n\t\"core/plugins/err/index.js\": 7793,\n\t\"core/plugins/err/reducers.js\": 3527,\n\t\"core/plugins/err/selectors.js\": 7667,\n\t\"core/plugins/filter/index.js\": 9978,\n\t\"core/plugins/filter/opsFilter.js\": 4309,\n\t\"core/plugins/json-schema-2020-12/components/Accordion/Accordion.jsx\": 7349,\n\t\"core/plugins/json-schema-2020-12/components/ExpandDeepButton/ExpandDeepButton.jsx\": 6867,\n\t\"core/plugins/json-schema-2020-12/components/JSONSchema/JSONSchema.jsx\": 2675,\n\t\"core/plugins/json-schema-2020-12/components/icons/ChevronRight.jsx\": 2260,\n\t\"core/plugins/json-schema-2020-12/components/keywords/$anchor.jsx\": 4922,\n\t\"core/plugins/json-schema-2020-12/components/keywords/$comment.jsx\": 4685,\n\t\"core/plugins/json-schema-2020-12/components/keywords/$defs.jsx\": 6418,\n\t\"core/plugins/json-schema-2020-12/components/keywords/$dynamicAnchor.jsx\": 1338,\n\t\"core/plugins/json-schema-2020-12/components/keywords/$dynamicRef.jsx\": 7655,\n\t\"core/plugins/json-schema-2020-12/components/keywords/$id.jsx\": 3460,\n\t\"core/plugins/json-schema-2020-12/components/keywords/$ref.jsx\": 2348,\n\t\"core/plugins/json-schema-2020-12/components/keywords/$schema.jsx\": 9359,\n\t\"core/plugins/json-schema-2020-12/components/keywords/$vocabulary/$vocabulary.jsx\": 7568,\n\t\"core/plugins/json-schema-2020-12/components/keywords/AdditionalProperties.jsx\": 5253,\n\t\"core/plugins/json-schema-2020-12/components/keywords/AllOf.jsx\": 6457,\n\t\"core/plugins/json-schema-2020-12/components/keywords/AnyOf.jsx\": 8776,\n\t\"core/plugins/json-schema-2020-12/components/keywords/Const.jsx\": 7308,\n\t\"core/plugins/json-schema-2020-12/components/keywords/Constraint/Constraint.jsx\": 9956,\n\t\"core/plugins/json-schema-2020-12/components/keywords/Contains.jsx\": 8993,\n\t\"core/plugins/json-schema-2020-12/components/keywords/ContentSchema.jsx\": 3484,\n\t\"core/plugins/json-schema-2020-12/components/keywords/Default.jsx\": 5148,\n\t\"core/plugins/json-schema-2020-12/components/keywords/DependentRequired/DependentRequired.jsx\": 4539,\n\t\"core/plugins/json-schema-2020-12/components/keywords/DependentSchemas.jsx\": 6076,\n\t\"core/plugins/json-schema-2020-12/components/keywords/Deprecated.jsx\": 6661,\n\t\"core/plugins/json-schema-2020-12/components/keywords/Description/Description.jsx\": 9446,\n\t\"core/plugins/json-schema-2020-12/components/keywords/Else.jsx\": 7207,\n\t\"core/plugins/json-schema-2020-12/components/keywords/Enum/Enum.jsx\": 1805,\n\t\"core/plugins/json-schema-2020-12/components/keywords/If.jsx\": 487,\n\t\"core/plugins/json-schema-2020-12/components/keywords/Items.jsx\": 9206,\n\t\"core/plugins/json-schema-2020-12/components/keywords/Not.jsx\": 5174,\n\t\"core/plugins/json-schema-2020-12/components/keywords/OneOf.jsx\": 3834,\n\t\"core/plugins/json-schema-2020-12/components/keywords/PatternProperties/PatternProperties.jsx\": 6746,\n\t\"core/plugins/json-schema-2020-12/components/keywords/PrefixItems.jsx\": 3971,\n\t\"core/plugins/json-schema-2020-12/components/keywords/Properties/Properties.jsx\": 5472,\n\t\"core/plugins/json-schema-2020-12/components/keywords/PropertyNames.jsx\": 2338,\n\t\"core/plugins/json-schema-2020-12/components/keywords/ReadOnly.jsx\": 6456,\n\t\"core/plugins/json-schema-2020-12/components/keywords/Then.jsx\": 7401,\n\t\"core/plugins/json-schema-2020-12/components/keywords/Title/Title.jsx\": 8137,\n\t\"core/plugins/json-schema-2020-12/components/keywords/Type.jsx\": 2285,\n\t\"core/plugins/json-schema-2020-12/components/keywords/UnevaluatedItems.jsx\": 5828,\n\t\"core/plugins/json-schema-2020-12/components/keywords/UnevaluatedProperties.jsx\": 6907,\n\t\"core/plugins/json-schema-2020-12/components/keywords/WriteOnly.jsx\": 5789,\n\t\"core/plugins/json-schema-2020-12/context.js\": 9006,\n\t\"core/plugins/json-schema-2020-12/fn.js\": 4121,\n\t\"core/plugins/json-schema-2020-12/hoc.jsx\": 5077,\n\t\"core/plugins/json-schema-2020-12/hooks.js\": 2603,\n\t\"core/plugins/json-schema-2020-12/index.js\": 7139,\n\t\"core/plugins/json-schema-2020-12/prop-types.js\": 6648,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/api/encoderAPI.js\": 9507,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/api/formatAPI.js\": 2906,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/api/mediaTypeAPI.js\": 537,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/class/EncoderRegistry.js\": 674,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/class/MediaTypeRegistry.js\": 3782,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/class/Registry.js\": 4215,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/core/constants.js\": 8338,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/core/example.js\": 3783,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/core/merge.js\": 7078,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/core/predicates.js\": 3084,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/core/random.js\": 5202,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/core/type.js\": 6276,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/core/utils.js\": 9346,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/encoders/7bit.js\": 1433,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/encoders/8bit.js\": 8509,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/encoders/base16.js\": 5709,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/encoders/base32.js\": 4180,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/encoders/base64.js\": 1967,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/encoders/binary.js\": 4366,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/encoders/quoted-printable.js\": 5037,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/date-time.js\": 4045,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/date.js\": 1456,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/double.js\": 560,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/duration.js\": 4299,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/email.js\": 3981,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/float.js\": 1890,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/hostname.js\": 9375,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/idn-email.js\": 4518,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/idn-hostname.js\": 273,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/int32.js\": 7864,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/int64.js\": 1726,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/ipv4.js\": 8793,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/ipv6.js\": 8269,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/iri-reference.js\": 5693,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/iri.js\": 3080,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/json-pointer.js\": 7856,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/media-types/application.js\": 5652,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/media-types/audio.js\": 4342,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/media-types/image.js\": 6724,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/media-types/text.js\": 5378,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/media-types/video.js\": 2974,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/password.js\": 3393,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/regex.js\": 4335,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/relative-json-pointer.js\": 375,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/time.js\": 5243,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/uri-reference.js\": 4692,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/uri-template.js\": 3829,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/uri.js\": 2978,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/generators/uuid.js\": 8859,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/index.js\": 8591,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/main.js\": 4277,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/types/array.js\": 8262,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/types/boolean.js\": 4108,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/types/index.js\": 3273,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/types/integer.js\": 8864,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/types/null.js\": 853,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/types/number.js\": 7742,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/types/object.js\": 6852,\n\t\"core/plugins/json-schema-2020-12/samples-extensions/fn/types/string.js\": 4522,\n\t\"core/plugins/layout/actions.js\": 5474,\n\t\"core/plugins/layout/index.js\": 6821,\n\t\"core/plugins/layout/reducers.js\": 5672,\n\t\"core/plugins/layout/selectors.js\": 4400,\n\t\"core/plugins/layout/spec-extensions/wrap-selector.js\": 8989,\n\t\"core/plugins/logs/index.js\": 9150,\n\t\"core/plugins/oas3/actions.js\": 7002,\n\t\"core/plugins/oas3/auth-extensions/wrap-selectors.js\": 3723,\n\t\"core/plugins/oas3/components/callbacks.jsx\": 3427,\n\t\"core/plugins/oas3/components/http-auth.jsx\": 6775,\n\t\"core/plugins/oas3/components/index.js\": 6467,\n\t\"core/plugins/oas3/components/operation-link.jsx\": 5757,\n\t\"core/plugins/oas3/components/operation-servers.jsx\": 6796,\n\t\"core/plugins/oas3/components/request-body-editor.jsx\": 5327,\n\t\"core/plugins/oas3/components/request-body.jsx\": 2458,\n\t\"core/plugins/oas3/components/servers-container.jsx\": 9928,\n\t\"core/plugins/oas3/components/servers.jsx\": 6617,\n\t\"core/plugins/oas3/helpers.jsx\": 7779,\n\t\"core/plugins/oas3/index.js\": 7451,\n\t\"core/plugins/oas3/reducers.js\": 2109,\n\t\"core/plugins/oas3/selectors.js\": 5065,\n\t\"core/plugins/oas3/spec-extensions/selectors.js\": 1741,\n\t\"core/plugins/oas3/spec-extensions/wrap-selectors.js\": 2044,\n\t\"core/plugins/oas3/wrap-components/auth-item.jsx\": 356,\n\t\"core/plugins/oas3/wrap-components/index.js\": 7761,\n\t\"core/plugins/oas3/wrap-components/json-schema-string.jsx\": 287,\n\t\"core/plugins/oas3/wrap-components/markdown.jsx\": 2460,\n\t\"core/plugins/oas3/wrap-components/model.jsx\": 3499,\n\t\"core/plugins/oas3/wrap-components/online-validator-badge.js\": 58,\n\t\"core/plugins/oas3/wrap-components/version-stamp.jsx\": 9487,\n\t\"core/plugins/oas31/after-load.js\": 7754,\n\t\"core/plugins/oas31/components/contact.jsx\": 9503,\n\t\"core/plugins/oas31/components/info.jsx\": 6133,\n\t\"core/plugins/oas31/components/json-schema-dialect.jsx\": 2562,\n\t\"core/plugins/oas31/components/license.jsx\": 1876,\n\t\"core/plugins/oas31/components/model/model.jsx\": 2718,\n\t\"core/plugins/oas31/components/models/models.jsx\": 263,\n\t\"core/plugins/oas31/components/version-pragma-filter.jsx\": 3429,\n\t\"core/plugins/oas31/components/webhooks.jsx\": 9508,\n\t\"core/plugins/oas31/fn.js\": 4380,\n\t\"core/plugins/oas31/index.js\": 9806,\n\t\"core/plugins/oas31/json-schema-2020-12-extensions/components/keywords/Description.jsx\": 5989,\n\t\"core/plugins/oas31/json-schema-2020-12-extensions/components/keywords/Discriminator/Discriminator.jsx\": 9525,\n\t\"core/plugins/oas31/json-schema-2020-12-extensions/components/keywords/Discriminator/DiscriminatorMapping.jsx\": 7749,\n\t\"core/plugins/oas31/json-schema-2020-12-extensions/components/keywords/Example.jsx\": 9450,\n\t\"core/plugins/oas31/json-schema-2020-12-extensions/components/keywords/ExternalDocs.jsx\": 5324,\n\t\"core/plugins/oas31/json-schema-2020-12-extensions/components/keywords/Properties.jsx\": 9023,\n\t\"core/plugins/oas31/json-schema-2020-12-extensions/components/keywords/Xml.jsx\": 3995,\n\t\"core/plugins/oas31/json-schema-2020-12-extensions/fn.js\": 5800,\n\t\"core/plugins/oas31/json-schema-2020-12-extensions/wrap-components/keywords/Default.jsx\": 4951,\n\t\"core/plugins/oas31/json-schema-2020-12-extensions/wrap-components/keywords/Description.jsx\": 809,\n\t\"core/plugins/oas31/json-schema-2020-12-extensions/wrap-components/keywords/Properties.jsx\": 7536,\n\t\"core/plugins/oas31/selectors.js\": 4280,\n\t\"core/plugins/oas31/spec-extensions/selectors.js\": 9305,\n\t\"core/plugins/oas31/spec-extensions/wrap-selectors.js\": 2884,\n\t\"core/plugins/oas31/wrap-components/contact.jsx\": 7423,\n\t\"core/plugins/oas31/wrap-components/info.jsx\": 284,\n\t\"core/plugins/oas31/wrap-components/license.jsx\": 6608,\n\t\"core/plugins/oas31/wrap-components/model.jsx\": 7042,\n\t\"core/plugins/oas31/wrap-components/models.jsx\": 2914,\n\t\"core/plugins/oas31/wrap-components/version-pragma-filter.jsx\": 1434,\n\t\"core/plugins/oas31/wrap-components/version-stamp.jsx\": 1122,\n\t\"core/plugins/on-complete/index.js\": 8560,\n\t\"core/plugins/request-snippets/fn.js\": 4624,\n\t\"core/plugins/request-snippets/index.js\": 6575,\n\t\"core/plugins/request-snippets/request-snippets.jsx\": 4206,\n\t\"core/plugins/request-snippets/selectors.js\": 4669,\n\t\"core/plugins/safe-render/components/error-boundary.jsx\": 6195,\n\t\"core/plugins/safe-render/components/fallback.jsx\": 9403,\n\t\"core/plugins/safe-render/fn.jsx\": 6189,\n\t\"core/plugins/safe-render/index.js\": 8102,\n\t\"core/plugins/samples/fn/get-json-sample-schema.js\": 2846,\n\t\"core/plugins/samples/fn/get-sample-schema.js\": 6132,\n\t\"core/plugins/samples/fn/get-xml-sample-schema.js\": 1169,\n\t\"core/plugins/samples/fn/get-yaml-sample-schema.js\": 9431,\n\t\"core/plugins/samples/fn/index.js\": 9812,\n\t\"core/plugins/samples/index.js\": 8883,\n\t\"core/plugins/spec/actions.js\": 7612,\n\t\"core/plugins/spec/index.js\": 7038,\n\t\"core/plugins/spec/reducers.js\": 32,\n\t\"core/plugins/spec/selectors.js\": 3881,\n\t\"core/plugins/spec/wrap-actions.js\": 7508,\n\t\"core/plugins/swagger-js/configs-wrap-actions.js\": 4852,\n\t\"core/plugins/swagger-js/index.js\": 9430,\n\t\"core/plugins/util/index.js\": 8525,\n\t\"core/plugins/view/fn.js\": 8347,\n\t\"core/plugins/view/index.js\": 3420,\n\t\"core/plugins/view/root-injects.jsx\": 3934\n};\n\n\nfunction webpackContext(req) {\n\tvar id = webpackContextResolve(req);\n\treturn __webpack_require__(id);\n}\nfunction webpackContextResolve(req) {\n\tif(!__webpack_require__.o(map, req)) {\n\t\tvar e = new Error(\"Cannot find module '\" + req + \"'\");\n\t\te.code = 'MODULE_NOT_FOUND';\n\t\tthrow e;\n\t}\n\treturn map[req];\n}\nwebpackContext.keys = function webpackContextKeys() {\n\treturn Object.keys(map);\n};\nwebpackContext.resolve = webpackContextResolve;\nmodule.exports = webpackContext;\nwebpackContext.id = 5102;","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_array_from_6be643d1__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_array_is_array_6a843f38__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_instance_bind_23a689fe__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_instance_concat_ad403b1a__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_instance_entries_97fed13d__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_instance_every_ac7bb0bc__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_instance_filter_13f270a8__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_instance_find_0ad1164d__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_instance_for_each_f55cb86b__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_instance_includes_c33ad5d5__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_instance_index_of_5fb826c6__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_instance_keys_3b8fec80__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_instance_map_868765ae__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_instance_reduce_e87b61a7__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_instance_slice_9832b507__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_instance_some_50ff1b2d__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_instance_sort_abe23e03__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_instance_trim_ca5b709e__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_json_stringify_1bf7a515__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_map_16a511c8__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_number_is_integer_a32e4569__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_object_assign_e13b6141__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_object_entries_20954bdf__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_object_from_entries_c9366fc2__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_object_keys_e09d3035__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_object_values_550c3b22__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_set_3488258a__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_set_timeout_d31e8027__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_url_4cfab046__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_weak_map_2eee9f61__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_weak_set_9e85a4f8__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_helpers_classPrivateFieldGet_c0aa81e1__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_helpers_defineProperty_807a2698__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_helpers_extends_d20d3ceb__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = __WEBPACK_EXTERNAL_MODULE_base64_js_f145eb6e__;","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_classnames__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = __WEBPACK_EXTERNAL_MODULE_ieee754__;","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"List\"]: () => __WEBPACK_EXTERNAL_MODULE_immutable__.List, [\"Map\"]: () => __WEBPACK_EXTERNAL_MODULE_immutable__.Map, [\"OrderedMap\"]: () => __WEBPACK_EXTERNAL_MODULE_immutable__.OrderedMap, [\"Seq\"]: () => __WEBPACK_EXTERNAL_MODULE_immutable__.Seq, [\"Set\"]: () => __WEBPACK_EXTERNAL_MODULE_immutable__.Set, [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_immutable__[\"default\"], [\"fromJS\"]: () => __WEBPACK_EXTERNAL_MODULE_immutable__.fromJS });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"JSON_SCHEMA\"]: () => __WEBPACK_EXTERNAL_MODULE_js_yaml_78384032__.JSON_SCHEMA, [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_js_yaml_78384032__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_lodash_get_9427f899__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_lodash_identity_75ffe4a7__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_lodash_isEmpty_e109fd6b__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_lodash_isFunction_f90b20d6__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_lodash_isPlainObject_116f2243__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_lodash_memoize_2b5bc477__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_lodash_some_5cd47809__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_prop_types_adfe8e31__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_randexp__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"Component\"]: () => __WEBPACK_EXTERNAL_MODULE_react__.Component, [\"PureComponent\"]: () => __WEBPACK_EXTERNAL_MODULE_react__.PureComponent, [\"createContext\"]: () => __WEBPACK_EXTERNAL_MODULE_react__.createContext, [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_react__[\"default\"], [\"forwardRef\"]: () => __WEBPACK_EXTERNAL_MODULE_react__.forwardRef, [\"useCallback\"]: () => __WEBPACK_EXTERNAL_MODULE_react__.useCallback, [\"useContext\"]: () => __WEBPACK_EXTERNAL_MODULE_react__.useContext, [\"useEffect\"]: () => __WEBPACK_EXTERNAL_MODULE_react__.useEffect, [\"useRef\"]: () => __WEBPACK_EXTERNAL_MODULE_react__.useRef, [\"useState\"]: () => __WEBPACK_EXTERNAL_MODULE_react__.useState });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"CopyToClipboard\"]: () => __WEBPACK_EXTERNAL_MODULE_react_copy_to_clipboard_5b11dd57__.CopyToClipboard });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_react_immutable_proptypes_89c7d083__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"applyMiddleware\"]: () => __WEBPACK_EXTERNAL_MODULE_redux__.applyMiddleware, [\"bindActionCreators\"]: () => __WEBPACK_EXTERNAL_MODULE_redux__.bindActionCreators, [\"compose\"]: () => __WEBPACK_EXTERNAL_MODULE_redux__.compose, [\"createStore\"]: () => __WEBPACK_EXTERNAL_MODULE_redux__.createStore });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"Remarkable\"]: () => __WEBPACK_EXTERNAL_MODULE_remarkable__.Remarkable });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"createSelector\"]: () => __WEBPACK_EXTERNAL_MODULE_reselect__.createSelector });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"serializeError\"]: () => __WEBPACK_EXTERNAL_MODULE_serialize_error_5f2df3e5__.serializeError });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"opId\"]: () => __WEBPACK_EXTERNAL_MODULE_swagger_client_es_helpers_4d7bea47__.opId });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nmodule.exports = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_url_parse_6456105f__[\"default\"] });","module.exports = require(\"core-js-pure/stable/object/define-property\");","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.g = (function() {\n\tif (typeof globalThis === 'object') return globalThis;\n\ttry {\n\t\treturn this || new Function('return this')();\n\t} catch (e) {\n\t\tif (typeof window === 'object') return window;\n\t}\n})();","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_instance_last_index_of_bbdfc000__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"combineReducers\"]: () => __WEBPACK_EXTERNAL_MODULE_redux_immutable_446c9f82__.combineReducers });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_lodash_merge_cf99375a__[\"default\"] });","import React from \"react\"\nimport { createStore, applyMiddleware, bindActionCreators, compose } from \"redux\"\nimport Im, { fromJS, Map } from \"immutable\"\nimport deepExtend from \"deep-extend\"\nimport { combineReducers } from \"redux-immutable\"\nimport { serializeError } from \"serialize-error\"\nimport merge from \"lodash/merge\"\nimport { NEW_THROWN_ERR } from \"corePlugins/err/actions\"\nimport win from \"core/window\"\n\nimport { systemThunkMiddleware, isFn, objMap, objReduce, isObject, isArray, isFunc } from \"core/utils\"\n\nconst idFn = a => a\n\n// Apply middleware that gets sandwitched between `dispatch` and the reducer function(s)\nfunction createStoreWithMiddleware(rootReducer, initialState, getSystem) {\n\n let middlwares = [\n // createLogger( {\n // stateTransformer: state => state && state.toJS()\n // } ),\n systemThunkMiddleware( getSystem )\n ]\n\n const composeEnhancers = win.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose\n\n return createStore(rootReducer, initialState, composeEnhancers(\n applyMiddleware( ...middlwares )\n ))\n}\n\nexport default class Store {\n\n constructor(opts={}) {\n deepExtend(this, {\n state: {},\n plugins: [],\n pluginsOptions: {},\n system: {\n configs: {},\n fn: {},\n components: {},\n rootInjects: {},\n statePlugins: {}\n },\n boundSystem: {},\n toolbox: {}\n }, opts)\n\n this.getSystem = this._getSystem.bind(this)\n\n // Bare system (nothing in it, besides the state)\n this.store = configureStore(idFn, fromJS(this.state), this.getSystem )\n\n // will be the system + Im, we can add more tools when we need to\n this.buildSystem(false)\n\n // Bootstrap plugins\n this.register(this.plugins)\n }\n\n getStore() {\n return this.store\n }\n\n register(plugins, rebuild=true) {\n var pluginSystem = combinePlugins(plugins, this.getSystem(), this.pluginsOptions)\n systemExtend(this.system, pluginSystem)\n if(rebuild) {\n this.buildSystem()\n }\n\n const needAnotherRebuild = callAfterLoad.call(this.system, plugins, this.getSystem())\n\n if(needAnotherRebuild) {\n this.buildSystem()\n }\n }\n\n buildSystem(buildReducer=true) {\n let dispatch = this.getStore().dispatch\n let getState = this.getStore().getState\n\n this.boundSystem = Object.assign({},\n this.getRootInjects(),\n this.getWrappedAndBoundActions(dispatch),\n this.getWrappedAndBoundSelectors(getState, this.getSystem),\n this.getStateThunks(getState),\n this.getFn(),\n this.getConfigs()\n )\n\n if(buildReducer)\n this.rebuildReducer()\n }\n\n _getSystem() {\n return this.boundSystem\n }\n\n getRootInjects() {\n return Object.assign({\n getSystem: this.getSystem,\n getStore: this.getStore.bind(this),\n getComponents: this.getComponents.bind(this),\n getState: this.getStore().getState,\n getConfigs: this._getConfigs.bind(this),\n Im,\n React\n }, this.system.rootInjects || {})\n }\n\n _getConfigs(){\n return this.system.configs\n }\n\n getConfigs() {\n return {\n configs: this.system.configs\n }\n }\n\n setConfigs(configs) {\n this.system.configs = configs\n }\n\n rebuildReducer() {\n this.store.replaceReducer(buildReducer(this.system.statePlugins))\n }\n\n /**\n * Generic getter from system.statePlugins\n *\n */\n getType(name) {\n let upName = name[0].toUpperCase() + name.slice(1)\n return objReduce(this.system.statePlugins, (val, namespace) => {\n let thing = val[name]\n if(thing)\n return {[namespace+upName]: thing}\n })\n }\n\n getSelectors() {\n return this.getType(\"selectors\")\n }\n\n getActions() {\n let actionHolders = this.getType(\"actions\")\n\n return objMap(actionHolders, (actions) => {\n return objReduce(actions, (action, actionName) => {\n if(isFn(action))\n return {[actionName]: action}\n })\n })\n }\n\n getWrappedAndBoundActions(dispatch) {\n let actionGroups = this.getBoundActions(dispatch)\n return objMap(actionGroups, (actions, actionGroupName) => {\n let wrappers = this.system.statePlugins[actionGroupName.slice(0,-7)].wrapActions\n if(wrappers) {\n return objMap(actions, (action, actionName) => {\n let wrap = wrappers[actionName]\n if(!wrap) {\n return action\n }\n\n if(!Array.isArray(wrap)) {\n wrap = [wrap]\n }\n return wrap.reduce((acc, fn) => {\n let newAction = (...args) => {\n return fn(acc, this.getSystem())(...args)\n }\n if(!isFn(newAction)) {\n throw new TypeError(\"wrapActions needs to return a function that returns a new function (ie the wrapped action)\")\n }\n return wrapWithTryCatch(newAction)\n }, action || Function.prototype)\n })\n }\n return actions\n })\n }\n\n getWrappedAndBoundSelectors(getState, getSystem) {\n let selectorGroups = this.getBoundSelectors(getState, getSystem)\n return objMap(selectorGroups, (selectors, selectorGroupName) => {\n let stateName = [selectorGroupName.slice(0, -9)] // selectors = 9 chars\n let wrappers = this.system.statePlugins[stateName].wrapSelectors\n if(wrappers) {\n return objMap(selectors, (selector, selectorName) => {\n let wrap = wrappers[selectorName]\n if(!wrap) {\n return selector\n }\n\n if(!Array.isArray(wrap)) {\n wrap = [wrap]\n }\n return wrap.reduce((acc, fn) => {\n let wrappedSelector = (...args) => {\n return fn(acc, this.getSystem())(getState().getIn(stateName), ...args)\n }\n if(!isFn(wrappedSelector)) {\n throw new TypeError(\"wrapSelector needs to return a function that returns a new function (ie the wrapped action)\")\n }\n return wrappedSelector\n }, selector || Function.prototype)\n })\n }\n return selectors\n })\n }\n\n getStates(state) {\n return Object.keys(this.system.statePlugins).reduce((obj, key) => {\n obj[key] = state.get(key)\n return obj\n }, {})\n }\n\n getStateThunks(getState) {\n return Object.keys(this.system.statePlugins).reduce((obj, key) => {\n obj[key] = ()=> getState().get(key)\n return obj\n }, {})\n }\n\n getFn() {\n return {\n fn: this.system.fn\n }\n }\n\n getComponents(component) {\n const res = this.system.components[component]\n\n if(Array.isArray(res)) {\n return res.reduce((ori, wrapper) => {\n return wrapper(ori, this.getSystem())\n })\n }\n if(typeof component !== \"undefined\") {\n return this.system.components[component]\n }\n\n return this.system.components\n }\n\n getBoundSelectors(getState, getSystem) {\n return objMap(this.getSelectors(), (obj, key) => {\n let stateName = [key.slice(0, -9)] // selectors = 9 chars\n const getNestedState = ()=> getState().getIn(stateName)\n\n return objMap(obj, (fn) => {\n return (...args) => {\n let res = wrapWithTryCatch(fn).apply(null, [getNestedState(), ...args])\n\n // If a selector returns a function, give it the system - for advanced usage\n if(typeof(res) === \"function\")\n res = wrapWithTryCatch(res)(getSystem())\n\n return res\n }\n })\n })\n }\n\n getBoundActions(dispatch) {\n\n dispatch = dispatch || this.getStore().dispatch\n\n const actions = this.getActions()\n\n const process = creator =>{\n if( typeof( creator ) !== \"function\" ) {\n return objMap(creator, prop => process(prop))\n }\n\n return ( ...args )=>{\n var action = null\n try{\n action = creator( ...args )\n }\n catch( e ){\n action = {type: NEW_THROWN_ERR, error: true, payload: serializeError(e) }\n }\n finally{\n return action // eslint-disable-line no-unsafe-finally\n }\n }\n\n }\n return objMap(actions, actionCreator => bindActionCreators( process( actionCreator ), dispatch ) )\n }\n\n getMapStateToProps() {\n return () => {\n return Object.assign({}, this.getSystem())\n }\n }\n\n getMapDispatchToProps(extras) {\n return (dispatch) => {\n return deepExtend({}, this.getWrappedAndBoundActions(dispatch), this.getFn(), extras)\n }\n }\n\n}\n\nfunction combinePlugins(plugins, toolbox, pluginOptions) {\n if(isObject(plugins) && !isArray(plugins)) {\n return merge({}, plugins)\n }\n\n if(isFunc(plugins)) {\n return combinePlugins(plugins(toolbox), toolbox, pluginOptions)\n }\n\n if(isArray(plugins)) {\n const dest = pluginOptions.pluginLoadType === \"chain\" ? toolbox.getComponents() : {}\n\n return plugins\n .map(plugin => combinePlugins(plugin, toolbox, pluginOptions))\n .reduce(systemExtend, dest)\n }\n\n return {}\n}\n\nfunction callAfterLoad(plugins, system, { hasLoaded } = {}) {\n let calledSomething = hasLoaded\n if(isObject(plugins) && !isArray(plugins)) {\n if(typeof plugins.afterLoad === \"function\") {\n calledSomething = true\n wrapWithTryCatch(plugins.afterLoad).call(this, system)\n }\n }\n\n if(isFunc(plugins))\n return callAfterLoad.call(this, plugins(system), system, { hasLoaded: calledSomething })\n\n if(isArray(plugins)) {\n return plugins.map(plugin => callAfterLoad.call(this, plugin, system, { hasLoaded: calledSomething }))\n }\n\n return calledSomething\n}\n\n// Wraps deepExtend, to account for certain fields, being wrappers.\n// Ie: we need to convert some fields into arrays, and append to them.\n// Rather than overwrite\nfunction systemExtend(dest={}, src={}) {\n\n if(!isObject(dest)) {\n return {}\n }\n if(!isObject(src)) {\n return dest\n }\n\n // Wrap components\n // Parses existing components in the system, and prepares them for wrapping via getComponents\n if(src.wrapComponents) {\n objMap(src.wrapComponents, (wrapperFn, key) => {\n const ori = dest.components && dest.components[key]\n if(ori && Array.isArray(ori)) {\n dest.components[key] = ori.concat([wrapperFn])\n delete src.wrapComponents[key]\n } else if(ori) {\n dest.components[key] = [ori, wrapperFn]\n delete src.wrapComponents[key]\n }\n })\n\n if(!Object.keys(src.wrapComponents).length) {\n // only delete wrapComponents if we've matched all of our wrappers to components\n // this handles cases where the component to wrap may be out of our scope,\n // but a higher recursive `combinePlugins` call will be able to handle it.\n delete src.wrapComponents\n }\n }\n\n\n // Account for wrapActions, make it an array and append to it\n // Modifies `src`\n // 80% of this code is just safe traversal. We need to address that ( ie: use a lib )\n const { statePlugins } = dest\n if(isObject(statePlugins)) {\n for(let namespace in statePlugins) {\n const namespaceObj = statePlugins[namespace]\n if(!isObject(namespaceObj)) {\n continue\n }\n\n const { wrapActions, wrapSelectors } = namespaceObj\n\n // process action wrapping\n if (isObject(wrapActions)) {\n for(let actionName in wrapActions) {\n let action = wrapActions[actionName]\n\n // This should only happen if dest is the first plugin, since invocations after that will ensure its an array\n if(!Array.isArray(action)) {\n action = [action]\n wrapActions[actionName] = action // Put the value inside an array\n }\n\n if(src && src.statePlugins && src.statePlugins[namespace] && src.statePlugins[namespace].wrapActions && src.statePlugins[namespace].wrapActions[actionName]) {\n src.statePlugins[namespace].wrapActions[actionName] = wrapActions[actionName].concat(src.statePlugins[namespace].wrapActions[actionName])\n }\n\n }\n }\n\n // process selector wrapping\n if (isObject(wrapSelectors)) {\n for(let selectorName in wrapSelectors) {\n let selector = wrapSelectors[selectorName]\n\n // This should only happen if dest is the first plugin, since invocations after that will ensure its an array\n if(!Array.isArray(selector)) {\n selector = [selector]\n wrapSelectors[selectorName] = selector // Put the value inside an array\n }\n\n if(src && src.statePlugins && src.statePlugins[namespace] && src.statePlugins[namespace].wrapSelectors && src.statePlugins[namespace].wrapSelectors[selectorName]) {\n src.statePlugins[namespace].wrapSelectors[selectorName] = wrapSelectors[selectorName].concat(src.statePlugins[namespace].wrapSelectors[selectorName])\n }\n\n }\n }\n }\n }\n\n return deepExtend(dest, src)\n}\n\nfunction buildReducer(states) {\n let reducerObj = objMap(states, (val) => {\n return val.reducers\n })\n return allReducers(reducerObj)\n}\n\nfunction allReducers(reducerSystem) {\n let reducers = Object.keys(reducerSystem).reduce((obj, key) => {\n obj[key] = makeReducer(reducerSystem[key])\n return obj\n },{})\n\n if(!Object.keys(reducers).length) {\n return idFn\n }\n\n return combineReducers(reducers)\n}\n\nfunction makeReducer(reducerObj) {\n return (state = new Map(), action) => {\n if(!reducerObj)\n return state\n\n let redFn = (reducerObj[action.type])\n if(redFn) {\n const res = wrapWithTryCatch(redFn)(state, action)\n // If the try/catch wrapper kicks in, we'll get null back...\n // in that case, we want to avoid making any changes to state\n return res === null ? state : res\n }\n return state\n }\n}\n\nfunction wrapWithTryCatch(fn, {\n logErrors = true\n} = {}) {\n if(typeof fn !== \"function\") {\n return fn\n }\n\n return function(...args) {\n try {\n return fn.call(this, ...args)\n } catch(e) {\n if(logErrors) {\n console.error(e)\n }\n return null\n }\n }\n}\n\nfunction configureStore(rootReducer, initialState, getSystem) {\n const store = createStoreWithMiddleware(rootReducer, initialState, getSystem)\n\n // if (module.hot) {\n // // Enable Webpack hot module replacement for reducers\n // module.hot.accept(\"reducers/index\", () => {\n // const nextRootReducer = require(\"reducers/index\")\n // store.replaceReducer(nextRootReducer)\n // })\n // }\n\n return store\n}\n","import React, { PureComponent } from \"react\"\nimport PropTypes from \"prop-types\"\nimport ImPropTypes from \"react-immutable-proptypes\"\nimport { opId } from \"swagger-client/es/helpers\"\nimport { Iterable, fromJS, Map } from \"immutable\"\n\nexport default class OperationContainer extends PureComponent {\n constructor(props, context) {\n super(props, context)\n\n const { tryItOutEnabled } = props.getConfigs()\n\n this.state = {\n tryItOutEnabled: tryItOutEnabled === true || tryItOutEnabled === \"true\",\n executeInProgress: false\n }\n }\n\n static propTypes = {\n op: PropTypes.instanceOf(Iterable).isRequired,\n tag: PropTypes.string.isRequired,\n path: PropTypes.string.isRequired,\n method: PropTypes.string.isRequired,\n operationId: PropTypes.string.isRequired,\n showSummary: PropTypes.bool.isRequired,\n isShown: PropTypes.bool.isRequired,\n jumpToKey: PropTypes.string.isRequired,\n allowTryItOut: PropTypes.bool,\n displayOperationId: PropTypes.bool,\n isAuthorized: PropTypes.bool,\n displayRequestDuration: PropTypes.bool,\n response: PropTypes.instanceOf(Iterable),\n request: PropTypes.instanceOf(Iterable),\n security: PropTypes.instanceOf(Iterable),\n isDeepLinkingEnabled: PropTypes.bool.isRequired,\n specPath: ImPropTypes.list.isRequired,\n getComponent: PropTypes.func.isRequired,\n authActions: PropTypes.object,\n oas3Actions: PropTypes.object,\n oas3Selectors: PropTypes.object,\n authSelectors: PropTypes.object,\n specActions: PropTypes.object.isRequired,\n specSelectors: PropTypes.object.isRequired,\n layoutActions: PropTypes.object.isRequired,\n layoutSelectors: PropTypes.object.isRequired,\n fn: PropTypes.object.isRequired,\n getConfigs: PropTypes.func.isRequired\n }\n\n static defaultProps = {\n showSummary: true,\n response: null,\n allowTryItOut: true,\n displayOperationId: false,\n displayRequestDuration: false\n }\n\n mapStateToProps(nextState, props) {\n const { op, layoutSelectors, getConfigs } = props\n const { docExpansion, deepLinking, displayOperationId, displayRequestDuration, supportedSubmitMethods } = getConfigs()\n const showSummary = layoutSelectors.showSummary()\n const operationId = op.getIn([\"operation\", \"__originalOperationId\"]) || op.getIn([\"operation\", \"operationId\"]) || opId(op.get(\"operation\"), props.path, props.method) || op.get(\"id\")\n const isShownKey = [\"operations\", props.tag, operationId]\n const isDeepLinkingEnabled = deepLinking && deepLinking !== \"false\"\n const allowTryItOut = supportedSubmitMethods.indexOf(props.method) >= 0 && (typeof props.allowTryItOut === \"undefined\" ?\n props.specSelectors.allowTryItOutFor(props.path, props.method) : props.allowTryItOut)\n const security = op.getIn([\"operation\", \"security\"]) || props.specSelectors.security()\n\n return {\n operationId,\n isDeepLinkingEnabled,\n showSummary,\n displayOperationId,\n displayRequestDuration,\n allowTryItOut,\n security,\n isAuthorized: props.authSelectors.isAuthorized(security),\n isShown: layoutSelectors.isShown(isShownKey, docExpansion === \"full\" ),\n jumpToKey: `paths.${props.path}.${props.method}`,\n response: props.specSelectors.responseFor(props.path, props.method),\n request: props.specSelectors.requestFor(props.path, props.method)\n }\n }\n\n componentDidMount() {\n const { isShown } = this.props\n const resolvedSubtree = this.getResolvedSubtree()\n\n if(isShown && resolvedSubtree === undefined) {\n this.requestResolvedSubtree()\n }\n }\n\n UNSAFE_componentWillReceiveProps(nextProps) {\n const { response, isShown } = nextProps\n const resolvedSubtree = this.getResolvedSubtree()\n\n if(response !== this.props.response) {\n this.setState({ executeInProgress: false })\n }\n\n if(isShown && resolvedSubtree === undefined) {\n this.requestResolvedSubtree()\n }\n }\n\n toggleShown =() => {\n let { layoutActions, tag, operationId, isShown } = this.props\n const resolvedSubtree = this.getResolvedSubtree()\n if(!isShown && resolvedSubtree === undefined) {\n // transitioning from collapsed to expanded\n this.requestResolvedSubtree()\n }\n layoutActions.show([\"operations\", tag, operationId], !isShown)\n }\n\n onCancelClick=() => {\n this.setState({tryItOutEnabled: !this.state.tryItOutEnabled})\n }\n\n onTryoutClick =() => {\n this.setState({tryItOutEnabled: !this.state.tryItOutEnabled})\n }\n\n onResetClick = (pathMethod) => {\n const defaultRequestBodyValue = this.props.oas3Selectors.selectDefaultRequestBodyValue(...pathMethod)\n this.props.oas3Actions.setRequestBodyValue({ value: defaultRequestBodyValue, pathMethod })\n }\n\n onExecute = () => {\n this.setState({ executeInProgress: true })\n }\n\n getResolvedSubtree = () => {\n const {\n specSelectors,\n path,\n method,\n specPath\n } = this.props\n\n if(specPath) {\n return specSelectors.specResolvedSubtree(specPath.toJS())\n }\n\n return specSelectors.specResolvedSubtree([\"paths\", path, method])\n }\n\n requestResolvedSubtree = () => {\n const {\n specActions,\n path,\n method,\n specPath\n } = this.props\n\n\n if(specPath) {\n return specActions.requestResolvedSubtree(specPath.toJS())\n }\n\n return specActions.requestResolvedSubtree([\"paths\", path, method])\n }\n\n render() {\n let {\n op: unresolvedOp,\n tag,\n path,\n method,\n security,\n isAuthorized,\n operationId,\n showSummary,\n isShown,\n jumpToKey,\n allowTryItOut,\n response,\n request,\n displayOperationId,\n displayRequestDuration,\n isDeepLinkingEnabled,\n specPath,\n specSelectors,\n specActions,\n getComponent,\n getConfigs,\n layoutSelectors,\n layoutActions,\n authActions,\n authSelectors,\n oas3Actions,\n oas3Selectors,\n fn\n } = this.props\n\n const Operation = getComponent( \"operation\" )\n\n const resolvedSubtree = this.getResolvedSubtree() || Map()\n\n const operationProps = fromJS({\n op: resolvedSubtree,\n tag,\n path,\n summary: unresolvedOp.getIn([\"operation\", \"summary\"]) || \"\",\n deprecated: resolvedSubtree.get(\"deprecated\") || unresolvedOp.getIn([\"operation\", \"deprecated\"]) || false,\n method,\n security,\n isAuthorized,\n operationId,\n originalOperationId: resolvedSubtree.getIn([\"operation\", \"__originalOperationId\"]),\n showSummary,\n isShown,\n jumpToKey,\n allowTryItOut,\n request,\n displayOperationId,\n displayRequestDuration,\n isDeepLinkingEnabled,\n executeInProgress: this.state.executeInProgress,\n tryItOutEnabled: this.state.tryItOutEnabled\n })\n\n return (\n \n )\n }\n\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\n\nexport default class App extends React.Component {\n\n getLayout() {\n let { getComponent, layoutSelectors } = this.props\n const layoutName = layoutSelectors.current()\n const Component = getComponent(layoutName, true)\n return Component ? Component : ()=>

    No layout defined for "{layoutName}"

    \n }\n\n render() {\n const Layout = this.getLayout()\n\n return (\n \n )\n }\n}\n\nApp.propTypes = {\n getComponent: PropTypes.func.isRequired,\n layoutSelectors: PropTypes.object.isRequired,\n}\n\nApp.defaultProps = {\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\n\nexport default class AuthorizationPopup extends React.Component {\n close =() => {\n let { authActions } = this.props\n\n authActions.showDefinitions(false)\n }\n\n render() {\n let { authSelectors, authActions, getComponent, errSelectors, specSelectors, fn: { AST = {} } } = this.props\n let definitions = authSelectors.shownDefinitions()\n const Auths = getComponent(\"auths\")\n\n return (\n
    \n
    \n
    \n
    \n
    \n
    \n

    Available authorizations

    \n \n
    \n
    \n\n {\n definitions.valueSeq().map(( definition, key ) => {\n return \n })\n }\n
    \n
    \n
    \n
    \n
    \n )\n }\n\n static propTypes = {\n fn: PropTypes.object.isRequired,\n getComponent: PropTypes.func.isRequired,\n authSelectors: PropTypes.object.isRequired,\n specSelectors: PropTypes.object.isRequired,\n errSelectors: PropTypes.object.isRequired,\n authActions: PropTypes.object.isRequired,\n }\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\n\nexport default class AuthorizeBtn extends React.Component {\n static propTypes = {\n onClick: PropTypes.func,\n isAuthorized: PropTypes.bool,\n showPopup: PropTypes.bool,\n getComponent: PropTypes.func.isRequired\n }\n\n render() {\n let { isAuthorized, showPopup, onClick, getComponent } = this.props\n\n //must be moved out of button component\n const AuthorizationPopup = getComponent(\"authorizationPopup\", true)\n\n return (\n
    \n \n { showPopup && }\n
    \n )\n }\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\n\nexport default class AuthorizeBtnContainer extends React.Component {\n\n static propTypes = {\n specActions: PropTypes.object.isRequired,\n specSelectors: PropTypes.object.isRequired,\n authActions: PropTypes.object.isRequired,\n authSelectors: PropTypes.object.isRequired,\n getComponent: PropTypes.func.isRequired\n }\n\n render () {\n const { authActions, authSelectors, specSelectors, getComponent} = this.props\n \n const securityDefinitions = specSelectors.securityDefinitions()\n const authorizableDefinitions = authSelectors.definitionsToAuthorize()\n\n const AuthorizeBtn = getComponent(\"authorizeBtn\")\n\n return securityDefinitions ? (\n authActions.showDefinitions(authorizableDefinitions)}\n isAuthorized={!!authSelectors.authorized().size}\n showPopup={!!authSelectors.shownDefinitions()}\n getComponent={getComponent}\n />\n ) : null\n }\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\n\nexport default class AuthorizeOperationBtn extends React.Component {\n static propTypes = {\n isAuthorized: PropTypes.bool.isRequired,\n onClick: PropTypes.func\n }\n\n onClick =(e) => {\n e.stopPropagation()\n let { onClick } = this.props\n\n if(onClick) {\n onClick()\n }\n }\n\n render() {\n let { isAuthorized } = this.props\n\n return (\n \n\n )\n }\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\nimport ImPropTypes from \"react-immutable-proptypes\"\n\nexport default class Auths extends React.Component {\n static propTypes = {\n definitions: ImPropTypes.iterable.isRequired,\n getComponent: PropTypes.func.isRequired,\n authSelectors: PropTypes.object.isRequired,\n authActions: PropTypes.object.isRequired,\n errSelectors: PropTypes.object.isRequired,\n specSelectors: PropTypes.object.isRequired\n }\n\n constructor(props, context) {\n super(props, context)\n\n this.state = {}\n }\n\n onAuthChange =(auth) => {\n let { name } = auth\n\n this.setState({ [name]: auth })\n }\n\n submitAuth =(e) => {\n e.preventDefault()\n\n let { authActions } = this.props\n authActions.authorizeWithPersistOption(this.state)\n }\n\n logoutClick =(e) => {\n e.preventDefault()\n\n let { authActions, definitions } = this.props\n let auths = definitions.map( (val, key) => {\n return key\n }).toArray()\n\n this.setState(auths.reduce((prev, auth) => {\n prev[auth] = \"\"\n return prev\n }, {}))\n\n authActions.logoutWithPersistOption(auths)\n }\n\n close =(e) => {\n e.preventDefault()\n let { authActions } = this.props\n\n authActions.showDefinitions(false)\n }\n\n render() {\n let { definitions, getComponent, authSelectors, errSelectors } = this.props\n const AuthItem = getComponent(\"AuthItem\")\n const Oauth2 = getComponent(\"oauth2\", true)\n const Button = getComponent(\"Button\")\n\n let authorized = authSelectors.authorized()\n\n let authorizedAuth = definitions.filter( (definition, key) => {\n return !!authorized.get(key)\n })\n\n let nonOauthDefinitions = definitions.filter( schema => schema.get(\"type\") !== \"oauth2\")\n let oauthDefinitions = definitions.filter( schema => schema.get(\"type\") === \"oauth2\")\n\n return (\n
    \n {\n !!nonOauthDefinitions.size &&
    \n {\n nonOauthDefinitions.map( (schema, name) => {\n return \n }).toArray()\n }\n
    \n {\n nonOauthDefinitions.size === authorizedAuth.size ? \n : \n }\n \n
    \n \n }\n\n {\n oauthDefinitions && oauthDefinitions.size ?
    \n
    \n

    Scopes are used to grant an application different levels of access to data on behalf of the end user. Each API may declare one or more scopes.

    \n

    API requires the following scopes. Select which ones you want to grant to Swagger UI.

    \n
    \n {\n definitions.filter( schema => schema.get(\"type\") === \"oauth2\")\n .map( (schema, name) =>{\n return (
    \n \n
    )\n }\n ).toArray()\n }\n
    : null\n }\n\n
    \n )\n }\n\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\nimport ImPropTypes from \"react-immutable-proptypes\"\n\nexport default class Auths extends React.Component {\n static propTypes = {\n authorized: ImPropTypes.orderedMap.isRequired,\n schema: ImPropTypes.orderedMap.isRequired,\n name: PropTypes.string.isRequired,\n getComponent: PropTypes.func.isRequired,\n onAuthChange: PropTypes.func.isRequired,\n errSelectors: PropTypes.object.isRequired,\n }\n\n render() {\n let {\n schema,\n name,\n getComponent,\n onAuthChange,\n authorized,\n errSelectors\n } = this.props\n const ApiKeyAuth = getComponent(\"apiKeyAuth\")\n const BasicAuth = getComponent(\"basicAuth\")\n\n let authEl\n\n const type = schema.get(\"type\")\n\n switch(type) {\n case \"apiKey\": authEl = \n break\n case \"basic\": authEl = \n break\n default: authEl =
    Unknown security definition type { type }
    \n }\n\n return (
    \n { authEl }\n
    )\n }\n\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\n\nexport default class AuthError extends React.Component {\n\n static propTypes = {\n error: PropTypes.object.isRequired\n }\n\n render() {\n let { error } = this.props\n\n let level = error.get(\"level\")\n let message = error.get(\"message\")\n let source = error.get(\"source\")\n\n return (\n
    \n { source } { level }\n { message }\n
    \n )\n }\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\n\nexport default class ApiKeyAuth extends React.Component {\n static propTypes = {\n authorized: PropTypes.object,\n getComponent: PropTypes.func.isRequired,\n errSelectors: PropTypes.object.isRequired,\n schema: PropTypes.object.isRequired,\n name: PropTypes.string.isRequired,\n onChange: PropTypes.func\n }\n\n constructor(props, context) {\n super(props, context)\n let { name, schema } = this.props\n let value = this.getValue()\n\n this.state = {\n name: name,\n schema: schema,\n value: value\n }\n }\n\n getValue () {\n let { name, authorized } = this.props\n\n return authorized && authorized.getIn([name, \"value\"])\n }\n\n onChange =(e) => {\n let { onChange } = this.props\n let value = e.target.value\n let newState = Object.assign({}, this.state, { value: value })\n\n this.setState(newState)\n onChange(newState)\n }\n\n render() {\n let { schema, getComponent, errSelectors, name } = this.props\n const Input = getComponent(\"Input\")\n const Row = getComponent(\"Row\")\n const Col = getComponent(\"Col\")\n const AuthError = getComponent(\"authError\")\n const Markdown = getComponent(\"Markdown\", true)\n const JumpToPath = getComponent(\"JumpToPath\", true)\n let value = this.getValue()\n let errors = errSelectors.allErrors().filter( err => err.get(\"authId\") === name)\n\n return (\n
    \n

    \n { name || schema.get(\"name\") } (apiKey)\n \n

    \n { value &&
    Authorized
    }\n \n \n \n \n

    Name: { schema.get(\"name\") }

    \n
    \n \n

    In: { schema.get(\"in\") }

    \n
    \n \n \n {\n value ? ****** \n : \n }\n \n {\n errors.valueSeq().map( (error, key) => {\n return \n } )\n }\n
    \n )\n }\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\nimport ImPropTypes from \"react-immutable-proptypes\"\n\nexport default class BasicAuth extends React.Component {\n static propTypes = {\n authorized: ImPropTypes.map,\n schema: ImPropTypes.map,\n getComponent: PropTypes.func.isRequired,\n onChange: PropTypes.func.isRequired,\n name: PropTypes.string.isRequired,\n errSelectors: PropTypes.object.isRequired,\n }\n\n constructor(props, context) {\n super(props, context)\n let { schema, name } = this.props\n\n let value = this.getValue()\n let username = value.username\n\n this.state = {\n name: name,\n schema: schema,\n value: !username ? {} : {\n username: username\n }\n }\n }\n\n getValue () {\n let { authorized, name } = this.props\n\n return authorized && authorized.getIn([name, \"value\"]) || {}\n }\n\n onChange =(e) => {\n let { onChange } = this.props\n let { value, name } = e.target\n\n let newValue = this.state.value\n newValue[name] = value\n\n this.setState({ value: newValue })\n\n onChange(this.state)\n }\n\n render() {\n let { schema, getComponent, name, errSelectors } = this.props\n const Input = getComponent(\"Input\")\n const Row = getComponent(\"Row\")\n const Col = getComponent(\"Col\")\n const AuthError = getComponent(\"authError\")\n const JumpToPath = getComponent(\"JumpToPath\", true)\n const Markdown = getComponent(\"Markdown\", true)\n let username = this.getValue().username\n let errors = errSelectors.allErrors().filter( err => err.get(\"authId\") === name)\n\n return (\n
    \n

    Basic authorization

    \n { username &&
    Authorized
    }\n \n \n \n \n \n {\n username ? { username } \n : \n }\n \n \n \n {\n username ? ****** \n : \n }\n \n {\n errors.valueSeq().map( (error, key) => {\n return \n } )\n }\n
    \n )\n }\n\n}\n","/**\n * @prettier\n */\n\nimport React from \"react\"\nimport PropTypes from \"prop-types\"\nimport ImPropTypes from \"react-immutable-proptypes\"\nimport { stringify } from \"core/utils\"\n\nexport default function Example(props) {\n const { example, showValue, getComponent, getConfigs } = props\n\n const Markdown = getComponent(\"Markdown\", true)\n const HighlightCode = getComponent(\"highlightCode\")\n\n if(!example) return null\n\n return (\n
    \n {example.get(\"description\") ? (\n
    \n
    Example Description
    \n

    \n \n

    \n
    \n ) : null}\n {showValue && example.has(\"value\") ? (\n
    \n
    Example Value
    \n \n
    \n ) : null}\n
    \n )\n}\n\nExample.propTypes = {\n example: ImPropTypes.map.isRequired,\n showValue: PropTypes.bool,\n getComponent: PropTypes.func.isRequired,\n getConfigs: PropTypes.func.getConfigs,\n}\n","/**\n * @prettier\n */\n\nimport React from \"react\"\nimport Im from \"immutable\"\nimport PropTypes from \"prop-types\"\nimport ImPropTypes from \"react-immutable-proptypes\"\n\nexport default class ExamplesSelect extends React.PureComponent {\n static propTypes = {\n examples: ImPropTypes.map.isRequired,\n onSelect: PropTypes.func,\n currentExampleKey: PropTypes.string,\n isModifiedValueAvailable: PropTypes.bool,\n isValueModified: PropTypes.bool,\n showLabels: PropTypes.bool,\n }\n\n static defaultProps = {\n examples: Im.Map({}),\n onSelect: (...args) =>\n console.log( // eslint-disable-line no-console\n // FIXME: remove before merging to master...\n `DEBUG: ExamplesSelect was not given an onSelect callback`,\n ...args\n ),\n currentExampleKey: null,\n showLabels: true,\n }\n\n _onSelect = (key, { isSyntheticChange = false } = {}) => {\n if (typeof this.props.onSelect === \"function\") {\n this.props.onSelect(key, {\n isSyntheticChange,\n })\n }\n }\n\n _onDomSelect = e => {\n if (typeof this.props.onSelect === \"function\") {\n const element = e.target.selectedOptions[0]\n const key = element.getAttribute(\"value\")\n\n this._onSelect(key, {\n isSyntheticChange: false,\n })\n }\n }\n\n getCurrentExample = () => {\n const { examples, currentExampleKey } = this.props\n\n const currentExamplePerProps = examples.get(currentExampleKey)\n\n const firstExamplesKey = examples.keySeq().first()\n const firstExample = examples.get(firstExamplesKey)\n\n return currentExamplePerProps || firstExample || Map({})\n }\n\n componentDidMount() {\n // this is the not-so-great part of ExamplesSelect... here we're\n // artificially kicking off an onSelect event in order to set a default\n // value in state. the consumer has the option to avoid this by checking\n // `isSyntheticEvent`, but we should really be doing this in a selector.\n // TODO: clean this up\n // FIXME: should this only trigger if `currentExamplesKey` is nullish?\n const { onSelect, examples } = this.props\n\n if (typeof onSelect === \"function\") {\n const firstExample = examples.first()\n const firstExampleKey = examples.keyOf(firstExample)\n\n this._onSelect(firstExampleKey, {\n isSyntheticChange: true,\n })\n }\n }\n\n UNSAFE_componentWillReceiveProps(nextProps) {\n const { currentExampleKey, examples } = nextProps\n if (examples !== this.props.examples && !examples.has(currentExampleKey)) {\n // examples have changed from under us, and the currentExampleKey is no longer\n // valid.\n const firstExample = examples.first()\n const firstExampleKey = examples.keyOf(firstExample)\n\n this._onSelect(firstExampleKey, {\n isSyntheticChange: true,\n })\n }\n }\n\n render() {\n const {\n examples,\n currentExampleKey,\n isValueModified,\n isModifiedValueAvailable,\n showLabels,\n } = this.props\n\n return (\n
    \n {\n showLabels ? (\n Examples: \n ) : null\n }\n \n {isModifiedValueAvailable ? (\n \n ) : null}\n {examples\n .map((example, exampleName) => {\n return (\n \n {example.get(\"summary\") || exampleName}\n \n )\n })\n .valueSeq()}\n \n
    \n )\n }\n}\n","/**\n * @prettier\n */\nimport React from \"react\"\nimport { Map, List } from \"immutable\"\nimport PropTypes from \"prop-types\"\nimport ImPropTypes from \"react-immutable-proptypes\"\n\nimport { stringify } from \"core/utils\"\n\n// This stateful component lets us avoid writing competing values (user\n// modifications vs example values) into global state, and the mess that comes\n// with that: tracking which of the two values are currently used for\n// Try-It-Out, which example a modified value came from, etc...\n//\n// The solution here is to retain the last user-modified value in\n// ExamplesSelectValueRetainer's component state, so that our global state can stay\n// clean, always simply being the source of truth for what value should be both\n// displayed to the user and used as a value during request execution.\n//\n// This approach/tradeoff was chosen in order to encapsulate the particular\n// logic of Examples within the Examples component tree, and to avoid\n// regressions within our current implementation elsewhere (non-Examples\n// definitions, OpenAPI 2.0, etc). A future refactor to global state might make\n// this component unnecessary.\n//\n// TL;DR: this is not our usual approach, but the choice was made consciously.\n\n// Note that `currentNamespace` isn't currently used anywhere!\n\nconst stringifyUnlessList = input =>\n List.isList(input) ? input : stringify(input)\n\nexport default class ExamplesSelectValueRetainer extends React.PureComponent {\n static propTypes = {\n examples: ImPropTypes.map,\n onSelect: PropTypes.func,\n updateValue: PropTypes.func, // mechanism to update upstream value\n userHasEditedBody: PropTypes.bool,\n getComponent: PropTypes.func.isRequired,\n currentUserInputValue: PropTypes.any,\n currentKey: PropTypes.string,\n currentNamespace: PropTypes.string,\n setRetainRequestBodyValueFlag: PropTypes.func.isRequired,\n // (also proxies props for Examples)\n }\n\n static defaultProps = {\n userHasEditedBody: false,\n examples: Map({}),\n currentNamespace: \"__DEFAULT__NAMESPACE__\",\n setRetainRequestBodyValueFlag: () => {\n // NOOP\n },\n onSelect: (...args) =>\n console.log( // eslint-disable-line no-console\n \"ExamplesSelectValueRetainer: no `onSelect` function was provided\",\n ...args\n ),\n updateValue: (...args) =>\n console.log( // eslint-disable-line no-console\n \"ExamplesSelectValueRetainer: no `updateValue` function was provided\",\n ...args\n ),\n }\n\n constructor(props) {\n super(props)\n\n const valueFromExample = this._getCurrentExampleValue()\n\n this.state = {\n // user edited: last value that came from the world around us, and didn't\n // match the current example's value\n // internal: last value that came from user selecting an Example\n [props.currentNamespace]: Map({\n lastUserEditedValue: this.props.currentUserInputValue,\n lastDownstreamValue: valueFromExample,\n isModifiedValueSelected:\n // valueFromExample !== undefined &&\n this.props.userHasEditedBody ||\n this.props.currentUserInputValue !== valueFromExample,\n }),\n }\n }\n\n componentWillUnmount() {\n this.props.setRetainRequestBodyValueFlag(false)\n }\n\n _getStateForCurrentNamespace = () => {\n const { currentNamespace } = this.props\n\n return (this.state[currentNamespace] || Map()).toObject()\n }\n\n _setStateForCurrentNamespace = obj => {\n const { currentNamespace } = this.props\n\n return this._setStateForNamespace(currentNamespace, obj)\n }\n\n _setStateForNamespace = (namespace, obj) => {\n const oldStateForNamespace = this.state[namespace] || Map()\n const newStateForNamespace = oldStateForNamespace.mergeDeep(obj)\n return this.setState({\n [namespace]: newStateForNamespace,\n })\n }\n\n _isCurrentUserInputSameAsExampleValue = () => {\n const { currentUserInputValue } = this.props\n\n const valueFromExample = this._getCurrentExampleValue()\n\n return valueFromExample === currentUserInputValue\n }\n\n _getValueForExample = (exampleKey, props) => {\n // props are accepted so that this can be used in UNSAFE_componentWillReceiveProps,\n // which has access to `nextProps`\n const { examples } = props || this.props\n return stringifyUnlessList(\n (examples || Map({})).getIn([exampleKey, \"value\"])\n )\n }\n\n _getCurrentExampleValue = props => {\n // props are accepted so that this can be used in UNSAFE_componentWillReceiveProps,\n // which has access to `nextProps`\n const { currentKey } = props || this.props\n return this._getValueForExample(currentKey, props || this.props)\n }\n\n _onExamplesSelect = (key, { isSyntheticChange } = {}, ...otherArgs) => {\n const {\n onSelect,\n updateValue,\n currentUserInputValue,\n userHasEditedBody,\n } = this.props\n const { lastUserEditedValue } = this._getStateForCurrentNamespace()\n\n const valueFromExample = this._getValueForExample(key)\n\n if (key === \"__MODIFIED__VALUE__\") {\n updateValue(stringifyUnlessList(lastUserEditedValue))\n return this._setStateForCurrentNamespace({\n isModifiedValueSelected: true,\n })\n }\n\n if (typeof onSelect === \"function\") {\n onSelect(key, { isSyntheticChange }, ...otherArgs)\n }\n\n this._setStateForCurrentNamespace({\n lastDownstreamValue: valueFromExample,\n isModifiedValueSelected:\n (isSyntheticChange && userHasEditedBody) ||\n (!!currentUserInputValue && currentUserInputValue !== valueFromExample),\n })\n\n // we never want to send up value updates from synthetic changes\n if (isSyntheticChange) return\n\n if (typeof updateValue === \"function\") {\n updateValue(stringifyUnlessList(valueFromExample))\n }\n }\n\n UNSAFE_componentWillReceiveProps(nextProps) {\n // update `lastUserEditedValue` as new currentUserInput values come in\n\n const {\n currentUserInputValue: newValue,\n examples,\n onSelect,\n userHasEditedBody,\n } = nextProps\n\n const {\n lastUserEditedValue,\n lastDownstreamValue,\n } = this._getStateForCurrentNamespace()\n\n const valueFromCurrentExample = this._getValueForExample(\n nextProps.currentKey,\n nextProps\n )\n\n const examplesMatchingNewValue = examples.filter(\n (example) =>\n example.get(\"value\") === newValue ||\n // sometimes data is stored as a string (e.g. in Request Bodies), so\n // let's check against a stringified version of our example too\n stringify(example.get(\"value\")) === newValue\n )\n\n if (examplesMatchingNewValue.size) {\n let key\n if(examplesMatchingNewValue.has(nextProps.currentKey))\n {\n key = nextProps.currentKey\n } else {\n key = examplesMatchingNewValue.keySeq().first()\n }\n onSelect(key, {\n isSyntheticChange: true,\n })\n } else if (\n newValue !== this.props.currentUserInputValue && // value has changed\n newValue !== lastUserEditedValue && // value isn't already tracked\n newValue !== lastDownstreamValue // value isn't what we've seen on the other side\n ) {\n this.props.setRetainRequestBodyValueFlag(true)\n this._setStateForNamespace(nextProps.currentNamespace, {\n lastUserEditedValue: nextProps.currentUserInputValue,\n isModifiedValueSelected:\n userHasEditedBody || newValue !== valueFromCurrentExample,\n })\n }\n }\n\n render() {\n const {\n currentUserInputValue,\n examples,\n currentKey,\n getComponent,\n userHasEditedBody,\n } = this.props\n const {\n lastDownstreamValue,\n lastUserEditedValue,\n isModifiedValueSelected,\n } = this._getStateForCurrentNamespace()\n\n const ExamplesSelect = getComponent(\"ExamplesSelect\")\n\n return (\n \n )\n }\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\nimport oauth2Authorize from \"core/oauth2-authorize\"\n\nexport default class Oauth2 extends React.Component {\n static propTypes = {\n name: PropTypes.string,\n authorized: PropTypes.object,\n getComponent: PropTypes.func.isRequired,\n schema: PropTypes.object.isRequired,\n authSelectors: PropTypes.object.isRequired,\n authActions: PropTypes.object.isRequired,\n errSelectors: PropTypes.object.isRequired,\n oas3Selectors: PropTypes.object.isRequired,\n specSelectors: PropTypes.object.isRequired,\n errActions: PropTypes.object.isRequired,\n getConfigs: PropTypes.any\n }\n\n constructor(props, context) {\n super(props, context)\n let { name, schema, authorized, authSelectors } = this.props\n let auth = authorized && authorized.get(name)\n let authConfigs = authSelectors.getConfigs() || {}\n let username = auth && auth.get(\"username\") || \"\"\n let clientId = auth && auth.get(\"clientId\") || authConfigs.clientId || \"\"\n let clientSecret = auth && auth.get(\"clientSecret\") || authConfigs.clientSecret || \"\"\n let passwordType = auth && auth.get(\"passwordType\") || \"basic\"\n let scopes = auth && auth.get(\"scopes\") || authConfigs.scopes || []\n if (typeof scopes === \"string\") {\n scopes = scopes.split(authConfigs.scopeSeparator || \" \")\n }\n\n this.state = {\n appName: authConfigs.appName,\n name: name,\n schema: schema,\n scopes: scopes,\n clientId: clientId,\n clientSecret: clientSecret,\n username: username,\n password: \"\",\n passwordType: passwordType\n }\n }\n\n close = (e) => {\n e.preventDefault()\n let { authActions } = this.props\n\n authActions.showDefinitions(false)\n }\n\n authorize =() => {\n let { authActions, errActions, getConfigs, authSelectors, oas3Selectors } = this.props\n let configs = getConfigs()\n let authConfigs = authSelectors.getConfigs()\n\n errActions.clear({authId: name,type: \"auth\", source: \"auth\"})\n oauth2Authorize({\n auth: this.state,\n currentServer: oas3Selectors.serverEffectiveValue(oas3Selectors.selectedServer()),\n authActions,\n errActions,\n configs,\n authConfigs\n })\n }\n\n onScopeChange =(e) => {\n let { target } = e\n let { checked } = target\n let scope = target.dataset.value\n\n if ( checked && this.state.scopes.indexOf(scope) === -1 ) {\n let newScopes = this.state.scopes.concat([scope])\n this.setState({ scopes: newScopes })\n } else if ( !checked && this.state.scopes.indexOf(scope) > -1) {\n this.setState({ scopes: this.state.scopes.filter((val) => val !== scope) })\n }\n }\n\n onInputChange =(e) => {\n let { target : { dataset : { name }, value } } = e\n let state = {\n [name]: value\n }\n\n this.setState(state)\n }\n\n selectScopes =(e) => {\n if (e.target.dataset.all) {\n this.setState({\n scopes: Array.from((this.props.schema.get(\"allowedScopes\") || this.props.schema.get(\"scopes\")).keys())\n })\n } else {\n this.setState({ scopes: [] })\n }\n }\n\n logout =(e) => {\n e.preventDefault()\n let { authActions, errActions, name } = this.props\n\n errActions.clear({authId: name, type: \"auth\", source: \"auth\"})\n authActions.logoutWithPersistOption([ name ])\n }\n\n render() {\n let {\n schema, getComponent, authSelectors, errSelectors, name, specSelectors\n } = this.props\n const Input = getComponent(\"Input\")\n const Row = getComponent(\"Row\")\n const Col = getComponent(\"Col\")\n const Button = getComponent(\"Button\")\n const AuthError = getComponent(\"authError\")\n const JumpToPath = getComponent(\"JumpToPath\", true)\n const Markdown = getComponent(\"Markdown\", true)\n const InitializedInput = getComponent(\"InitializedInput\")\n\n const { isOAS3 } = specSelectors\n\n let oidcUrl = isOAS3() ? schema.get(\"openIdConnectUrl\") : null\n\n // Auth type consts\n const AUTH_FLOW_IMPLICIT = \"implicit\"\n const AUTH_FLOW_PASSWORD = \"password\"\n const AUTH_FLOW_ACCESS_CODE = isOAS3() ? (oidcUrl ? \"authorization_code\" : \"authorizationCode\") : \"accessCode\"\n const AUTH_FLOW_APPLICATION = isOAS3() ? (oidcUrl ? \"client_credentials\" : \"clientCredentials\") : \"application\"\n\n let authConfigs = authSelectors.getConfigs() || {}\n let isPkceCodeGrant = !!authConfigs.usePkceWithAuthorizationCodeGrant\n\n let flow = schema.get(\"flow\")\n let flowToDisplay = flow === AUTH_FLOW_ACCESS_CODE && isPkceCodeGrant ? flow + \" with PKCE\" : flow\n let scopes = schema.get(\"allowedScopes\") || schema.get(\"scopes\")\n let authorizedAuth = authSelectors.authorized().get(name)\n let isAuthorized = !!authorizedAuth\n let errors = errSelectors.allErrors().filter( err => err.get(\"authId\") === name)\n let isValid = !errors.filter( err => err.get(\"source\") === \"validation\").size\n let description = schema.get(\"description\")\n\n return (\n
    \n

    {name} (OAuth2, { flowToDisplay })

    \n { !this.state.appName ? null :
    Application: { this.state.appName }
    }\n { description && }\n\n { isAuthorized &&
    Authorized
    }\n\n { oidcUrl &&

    OpenID Connect URL: { oidcUrl }

    }\n { ( flow === AUTH_FLOW_IMPLICIT || flow === AUTH_FLOW_ACCESS_CODE ) &&

    Authorization URL: { schema.get(\"authorizationUrl\") }

    }\n { ( flow === AUTH_FLOW_PASSWORD || flow === AUTH_FLOW_ACCESS_CODE || flow === AUTH_FLOW_APPLICATION ) &&

    Token URL: { schema.get(\"tokenUrl\") }

    }\n

    Flow: { flowToDisplay }

    \n\n {\n flow !== AUTH_FLOW_PASSWORD ? null\n : \n \n \n {\n isAuthorized ? { this.state.username } \n : \n \n \n }\n \n {\n\n }\n \n \n {\n isAuthorized ? ****** \n : \n \n \n }\n \n \n \n {\n isAuthorized ? { this.state.passwordType } \n : \n \n \n }\n \n \n }\n {\n ( flow === AUTH_FLOW_APPLICATION || flow === AUTH_FLOW_IMPLICIT || flow === AUTH_FLOW_ACCESS_CODE || flow === AUTH_FLOW_PASSWORD ) &&\n ( !isAuthorized || isAuthorized && this.state.clientId) && \n \n {\n isAuthorized ? ****** \n : \n \n \n }\n \n }\n\n {\n ( (flow === AUTH_FLOW_APPLICATION || flow === AUTH_FLOW_ACCESS_CODE || flow === AUTH_FLOW_PASSWORD) && \n \n {\n isAuthorized ? ****** \n : \n \n \n }\n\n \n )}\n\n {\n !isAuthorized && scopes && scopes.size ?
    \n

    \n Scopes:\n select all\n select none\n

    \n { scopes.map((description, name) => {\n return (\n \n
    \n \n \n
    \n
    \n )\n }).toArray()\n }\n
    : null\n }\n\n {\n errors.valueSeq().map( (error, key) => {\n return \n } )\n }\n
    \n { isValid &&\n ( isAuthorized ? \n : \n )\n }\n \n
    \n\n
    \n )\n }\n}\n","import parseUrl from \"url-parse\"\nimport Im from \"immutable\"\nimport { btoa, sanitizeUrl, generateCodeVerifier, createCodeChallenge } from \"core/utils\"\n\nexport default function authorize ( { auth, authActions, errActions, configs, authConfigs={}, currentServer } ) {\n let { schema, scopes, name, clientId } = auth\n let flow = schema.get(\"flow\")\n let query = []\n\n switch (flow) {\n case \"password\":\n authActions.authorizePassword(auth)\n return\n\n case \"application\":\n authActions.authorizeApplication(auth)\n return\n\n case \"accessCode\":\n query.push(\"response_type=code\")\n break\n\n case \"implicit\":\n query.push(\"response_type=token\")\n break\n\n case \"clientCredentials\":\n case \"client_credentials\":\n // OAS3\n authActions.authorizeApplication(auth)\n return\n\n case \"authorizationCode\":\n case \"authorization_code\":\n // OAS3\n query.push(\"response_type=code\")\n break\n }\n\n if (typeof clientId === \"string\") {\n query.push(\"client_id=\" + encodeURIComponent(clientId))\n }\n\n let redirectUrl = configs.oauth2RedirectUrl\n\n // todo move to parser\n if (typeof redirectUrl === \"undefined\") {\n errActions.newAuthErr( {\n authId: name,\n source: \"validation\",\n level: \"error\",\n message: \"oauth2RedirectUrl configuration is not passed. Oauth2 authorization cannot be performed.\"\n })\n return\n }\n query.push(\"redirect_uri=\" + encodeURIComponent(redirectUrl))\n\n let scopesArray = []\n if (Array.isArray(scopes)) {\n scopesArray = scopes\n } else if (Im.List.isList(scopes)) {\n scopesArray = scopes.toArray()\n }\n\n if (scopesArray.length > 0) {\n let scopeSeparator = authConfigs.scopeSeparator || \" \"\n\n query.push(\"scope=\" + encodeURIComponent(scopesArray.join(scopeSeparator)))\n }\n\n let state = btoa(new Date())\n\n query.push(\"state=\" + encodeURIComponent(state))\n\n if (typeof authConfigs.realm !== \"undefined\") {\n query.push(\"realm=\" + encodeURIComponent(authConfigs.realm))\n }\n\n if ((flow === \"authorizationCode\" || flow === \"authorization_code\" || flow === \"accessCode\") && authConfigs.usePkceWithAuthorizationCodeGrant) {\n const codeVerifier = generateCodeVerifier()\n const codeChallenge = createCodeChallenge(codeVerifier)\n\n query.push(\"code_challenge=\" + codeChallenge)\n query.push(\"code_challenge_method=S256\")\n\n // storing the Code Verifier so it can be sent to the token endpoint\n // when exchanging the Authorization Code for an Access Token\n auth.codeVerifier = codeVerifier\n }\n\n let { additionalQueryStringParams } = authConfigs\n\n for (let key in additionalQueryStringParams) {\n if (typeof additionalQueryStringParams[key] !== \"undefined\") {\n query.push([key, additionalQueryStringParams[key]].map(encodeURIComponent).join(\"=\"))\n }\n }\n\n const authorizationUrl = schema.get(\"authorizationUrl\")\n let sanitizedAuthorizationUrl\n if (currentServer) {\n // OpenAPI 3\n sanitizedAuthorizationUrl = parseUrl(\n sanitizeUrl(authorizationUrl),\n currentServer,\n true\n ).toString()\n } else {\n sanitizedAuthorizationUrl = sanitizeUrl(authorizationUrl)\n }\n let url = [sanitizedAuthorizationUrl, query.join(\"&\")].join(authorizationUrl.indexOf(\"?\") === -1 ? \"?\" : \"&\")\n\n // pass action authorizeOauth2 and authentication data through window\n // to authorize with oauth2\n\n let callback\n if (flow === \"implicit\") {\n callback = authActions.preAuthorizeImplicit\n } else if (authConfigs.useBasicAuthenticationWithAccessCodeGrant) {\n callback = authActions.authorizeAccessCodeWithBasicAuthentication\n } else {\n callback = authActions.authorizeAccessCodeWithFormParams\n }\n\n authActions.authPopup(url, {\n auth: auth,\n state: state,\n redirectUrl: redirectUrl,\n callback: callback,\n errCb: errActions.newAuthErr\n })\n}\n","import React, { Component } from \"react\"\nimport PropTypes from \"prop-types\"\n\nexport default class Clear extends Component {\n\n onClick =() => {\n let { specActions, path, method } = this.props\n specActions.clearResponse( path, method )\n specActions.clearRequest( path, method )\n }\n\n render(){\n return (\n \n )\n }\n\n static propTypes = {\n specActions: PropTypes.object.isRequired,\n path: PropTypes.string.isRequired,\n method: PropTypes.string.isRequired,\n }\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\nimport ImPropTypes from \"react-immutable-proptypes\"\n\nconst Headers = ( { headers } )=>{\n return (\n
    \n
    Response headers
    \n
    {headers}
    \n
    )\n}\nHeaders.propTypes = {\n headers: PropTypes.array.isRequired\n}\n\nconst Duration = ( { duration } ) => {\n return (\n
    \n
    Request duration
    \n
    {duration} ms
    \n
    \n )\n}\nDuration.propTypes = {\n duration: PropTypes.number.isRequired\n}\n\n\nexport default class LiveResponse extends React.Component {\n static propTypes = {\n response: ImPropTypes.map,\n path: PropTypes.string.isRequired,\n method: PropTypes.string.isRequired,\n displayRequestDuration: PropTypes.bool.isRequired,\n specSelectors: PropTypes.object.isRequired,\n getComponent: PropTypes.func.isRequired,\n getConfigs: PropTypes.func.isRequired\n }\n\n shouldComponentUpdate(nextProps) {\n // BUG: props.response is always coming back as a new Immutable instance\n // same issue as responses.jsx (tryItOutResponse)\n return this.props.response !== nextProps.response\n || this.props.path !== nextProps.path\n || this.props.method !== nextProps.method\n || this.props.displayRequestDuration !== nextProps.displayRequestDuration\n }\n\n render() {\n const { response, getComponent, getConfigs, displayRequestDuration, specSelectors, path, method } = this.props\n const { showMutatedRequest, requestSnippetsEnabled } = getConfigs()\n\n const curlRequest = showMutatedRequest ? specSelectors.mutatedRequestFor(path, method) : specSelectors.requestFor(path, method)\n const status = response.get(\"status\")\n const url = curlRequest.get(\"url\")\n const headers = response.get(\"headers\").toJS()\n const notDocumented = response.get(\"notDocumented\")\n const isError = response.get(\"error\")\n const body = response.get(\"text\")\n const duration = response.get(\"duration\")\n const headersKeys = Object.keys(headers)\n const contentType = headers[\"content-type\"] || headers[\"Content-Type\"]\n\n const ResponseBody = getComponent(\"responseBody\")\n const returnObject = headersKeys.map(key => {\n var joinedHeaders = Array.isArray(headers[key]) ? headers[key].join() : headers[key]\n return {key}: {joinedHeaders} \n })\n const hasHeaders = returnObject.length !== 0\n const Markdown = getComponent(\"Markdown\", true)\n const RequestSnippets = getComponent(\"RequestSnippets\", true)\n const Curl = getComponent(\"curl\")\n\n return (\n
    \n { curlRequest && (requestSnippetsEnabled === true || requestSnippetsEnabled === \"true\"\n ? \n : ) }\n { url &&
    \n
    \n

    Request URL

    \n
    {url}
    \n
    \n
    \n }\n

    Server response

    \n \n \n \n \n \n \n \n \n \n \n \n \n \n
    CodeDetails
    \n { status }\n {\n notDocumented ?
    \n Undocumented \n
    \n : null\n }\n
    \n {\n isError ? \n : null\n }\n {\n body ? \n : null\n }\n {\n hasHeaders ? : null\n }\n {\n displayRequestDuration && duration ? : null\n }\n
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\nimport Im from \"immutable\"\n\nexport default class Operations extends React.Component {\n\n static propTypes = {\n specSelectors: PropTypes.object.isRequired,\n specActions: PropTypes.object.isRequired,\n oas3Actions: PropTypes.object.isRequired,\n getComponent: PropTypes.func.isRequired,\n oas3Selectors: PropTypes.func.isRequired,\n layoutSelectors: PropTypes.object.isRequired,\n layoutActions: PropTypes.object.isRequired,\n authActions: PropTypes.object.isRequired,\n authSelectors: PropTypes.object.isRequired,\n getConfigs: PropTypes.func.isRequired,\n fn: PropTypes.func.isRequired\n }\n\n render() {\n let {\n specSelectors,\n } = this.props\n\n const taggedOps = specSelectors.taggedOperations()\n\n if(taggedOps.size === 0) {\n return

    No operations defined in spec!

    \n }\n\n return (\n
    \n { taggedOps.map(this.renderOperationTag).toArray() }\n { taggedOps.size < 1 ?

    No operations defined in spec!

    : null }\n
    \n )\n }\n\n renderOperationTag = (tagObj, tag) => {\n const {\n specSelectors,\n getComponent,\n oas3Selectors,\n layoutSelectors,\n layoutActions,\n getConfigs,\n } = this.props\n const validOperationMethods = specSelectors.validOperationMethods()\n const OperationContainer = getComponent(\"OperationContainer\", true)\n const OperationTag = getComponent(\"OperationTag\")\n const operations = tagObj.get(\"operations\")\n return (\n \n
    \n {\n operations.map(op => {\n const path = op.get(\"path\")\n const method = op.get(\"method\")\n const specPath = Im.List([\"paths\", path, method])\n\n if (validOperationMethods.indexOf(method) === -1) {\n return null\n }\n\n return (\n \n )\n }).toArray()\n }\n
    \n \n )\n }\n\n}\n\nOperations.propTypes = {\n layoutActions: PropTypes.object.isRequired,\n specSelectors: PropTypes.object.isRequired,\n specActions: PropTypes.object.isRequired,\n layoutSelectors: PropTypes.object.isRequired,\n getComponent: PropTypes.func.isRequired,\n fn: PropTypes.object.isRequired\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\nimport ImPropTypes from \"react-immutable-proptypes\"\nimport Im from \"immutable\"\nimport { createDeepLinkPath, escapeDeepLinkPath, sanitizeUrl } from \"core/utils\"\nimport { safeBuildUrl } from \"core/utils/url\"\nimport { isFunc } from \"core/utils\"\n\nexport default class OperationTag extends React.Component {\n\n static defaultProps = {\n tagObj: Im.fromJS({}),\n tag: \"\",\n }\n\n static propTypes = {\n tagObj: ImPropTypes.map.isRequired,\n tag: PropTypes.string.isRequired,\n\n oas3Selectors: PropTypes.func.isRequired,\n layoutSelectors: PropTypes.object.isRequired,\n layoutActions: PropTypes.object.isRequired,\n\n getConfigs: PropTypes.func.isRequired,\n getComponent: PropTypes.func.isRequired,\n\n specUrl: PropTypes.string.isRequired,\n\n children: PropTypes.element,\n }\n\n render() {\n const {\n tagObj,\n tag,\n children,\n oas3Selectors,\n layoutSelectors,\n layoutActions,\n getConfigs,\n getComponent,\n specUrl,\n } = this.props\n\n let {\n docExpansion,\n deepLinking,\n } = getConfigs()\n\n const isDeepLinkingEnabled = deepLinking && deepLinking !== \"false\"\n\n const Collapse = getComponent(\"Collapse\")\n const Markdown = getComponent(\"Markdown\", true)\n const DeepLink = getComponent(\"DeepLink\")\n const Link = getComponent(\"Link\")\n\n let tagDescription = tagObj.getIn([\"tagDetails\", \"description\"], null)\n let tagExternalDocsDescription = tagObj.getIn([\"tagDetails\", \"externalDocs\", \"description\"])\n let rawTagExternalDocsUrl = tagObj.getIn([\"tagDetails\", \"externalDocs\", \"url\"])\n let tagExternalDocsUrl\n if (isFunc(oas3Selectors) && isFunc(oas3Selectors.selectedServer)) {\n tagExternalDocsUrl = safeBuildUrl(rawTagExternalDocsUrl, specUrl, { selectedServer: oas3Selectors.selectedServer() })\n } else {\n tagExternalDocsUrl = rawTagExternalDocsUrl\n }\n\n let isShownKey = [\"operations-tag\", tag]\n let showTag = layoutSelectors.isShown(isShownKey, docExpansion === \"full\" || docExpansion === \"list\")\n\n return (\n
    \n\n layoutActions.show(isShownKey, !showTag)}\n className={!tagDescription ? \"opblock-tag no-desc\" : \"opblock-tag\"}\n id={isShownKey.map(v => escapeDeepLinkPath(v)).join(\"-\")}\n data-tag={tag}\n data-is-open={showTag}\n >\n \n {!tagDescription ? :\n \n \n \n }\n\n {!tagExternalDocsUrl ? null :\n
    \n \n e.stopPropagation()}\n target=\"_blank\"\n >{tagExternalDocsDescription || tagExternalDocsUrl}\n \n
    \n }\n\n\n layoutActions.show(isShownKey, !showTag)}>\n\n \n \n \n \n \n\n \n {children}\n \n
    \n )\n }\n}\n","import React, { PureComponent } from \"react\"\nimport PropTypes from \"prop-types\"\nimport { getList } from \"core/utils\"\nimport { getExtensions, sanitizeUrl, escapeDeepLinkPath } from \"core/utils\"\nimport { safeBuildUrl } from \"core/utils/url\"\nimport { Iterable, List } from \"immutable\"\nimport ImPropTypes from \"react-immutable-proptypes\"\n\n\nexport default class Operation extends PureComponent {\n static propTypes = {\n specPath: ImPropTypes.list.isRequired,\n operation: PropTypes.instanceOf(Iterable).isRequired,\n summary: PropTypes.string,\n response: PropTypes.instanceOf(Iterable),\n request: PropTypes.instanceOf(Iterable),\n\n toggleShown: PropTypes.func.isRequired,\n onTryoutClick: PropTypes.func.isRequired,\n onResetClick: PropTypes.func.isRequired,\n onCancelClick: PropTypes.func.isRequired,\n onExecute: PropTypes.func.isRequired,\n\n getComponent: PropTypes.func.isRequired,\n getConfigs: PropTypes.func.isRequired,\n authActions: PropTypes.object,\n authSelectors: PropTypes.object,\n specActions: PropTypes.object.isRequired,\n specSelectors: PropTypes.object.isRequired,\n oas3Actions: PropTypes.object.isRequired,\n oas3Selectors: PropTypes.object.isRequired,\n layoutActions: PropTypes.object.isRequired,\n layoutSelectors: PropTypes.object.isRequired,\n fn: PropTypes.object.isRequired\n }\n\n static defaultProps = {\n operation: null,\n response: null,\n request: null,\n specPath: List(),\n summary: \"\"\n }\n\n render() {\n let {\n specPath,\n response,\n request,\n toggleShown,\n onTryoutClick,\n onResetClick,\n onCancelClick,\n onExecute,\n fn,\n getComponent,\n getConfigs,\n specActions,\n specSelectors,\n authActions,\n authSelectors,\n oas3Actions,\n oas3Selectors\n } = this.props\n let operationProps = this.props.operation\n\n let {\n deprecated,\n isShown,\n path,\n method,\n op,\n tag,\n operationId,\n allowTryItOut,\n displayRequestDuration,\n tryItOutEnabled,\n executeInProgress\n } = operationProps.toJS()\n\n let {\n description,\n externalDocs,\n schemes\n } = op\n\n const externalDocsUrl = externalDocs ? safeBuildUrl(externalDocs.url, specSelectors.url(), { selectedServer: oas3Selectors.selectedServer() }) : \"\"\n let operation = operationProps.getIn([\"op\"])\n let responses = operation.get(\"responses\")\n let parameters = getList(operation, [\"parameters\"])\n let operationScheme = specSelectors.operationScheme(path, method)\n let isShownKey = [\"operations\", tag, operationId]\n let extensions = getExtensions(operation)\n\n const Responses = getComponent(\"responses\")\n const Parameters = getComponent( \"parameters\" )\n const Execute = getComponent( \"execute\" )\n const Clear = getComponent( \"clear\" )\n const Collapse = getComponent( \"Collapse\" )\n const Markdown = getComponent(\"Markdown\", true)\n const Schemes = getComponent( \"schemes\" )\n const OperationServers = getComponent( \"OperationServers\" )\n const OperationExt = getComponent( \"OperationExt\" )\n const OperationSummary = getComponent( \"OperationSummary\" )\n const Link = getComponent( \"Link\" )\n\n const { showExtensions } = getConfigs()\n\n // Merge in Live Response\n if(responses && response && response.size > 0) {\n let notDocumented = !responses.get(String(response.get(\"status\"))) && !responses.get(\"default\")\n response = response.set(\"notDocumented\", notDocumented)\n }\n\n let onChangeKey = [ path, method ] // Used to add values to _this_ operation ( indexed by path and method )\n\n const validationErrors = specSelectors.validationErrors([path, method])\n\n return (\n
    \n \n \n
    \n { (operation && operation.size) || operation === null ? null :\n \n }\n { deprecated &&

    Warning: Deprecated

    }\n { description &&\n
    \n
    \n \n
    \n
    \n }\n {\n externalDocsUrl ?\n
    \n

    Find more details

    \n
    \n {externalDocs.description &&\n \n \n \n }\n {externalDocsUrl}\n
    \n
    : null\n }\n\n { !operation || !operation.size ? null :\n \n }\n\n { !tryItOutEnabled ? null :\n \n }\n\n {!tryItOutEnabled || !allowTryItOut ? null : schemes && schemes.size ?
    \n \n
    : null\n }\n\n { !tryItOutEnabled || !allowTryItOut || validationErrors.length <= 0 ? null :
    \n Please correct the following validation errors and try again.\n
      \n { validationErrors.map((error, index) =>
    • { error }
    • ) }\n
    \n
    \n }\n\n
    \n { !tryItOutEnabled || !allowTryItOut ? null :\n\n \n }\n\n { (!tryItOutEnabled || !response || !allowTryItOut) ? null :\n \n }\n
    \n\n {executeInProgress ?
    : null}\n\n { !responses ? null :\n \n }\n\n { !showExtensions || !extensions.size ? null :\n \n }\n
    \n
    \n
    \n )\n }\n\n}\n","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_lodash_toString_da931f05__[\"default\"] });","import React, { PureComponent } from \"react\"\nimport PropTypes from \"prop-types\"\nimport { Iterable, List } from \"immutable\"\nimport ImPropTypes from \"react-immutable-proptypes\"\nimport toString from \"lodash/toString\"\n\n\nexport default class OperationSummary extends PureComponent {\n\n static propTypes = {\n specPath: ImPropTypes.list.isRequired,\n operationProps: PropTypes.instanceOf(Iterable).isRequired,\n isShown: PropTypes.bool.isRequired,\n toggleShown: PropTypes.func.isRequired,\n getComponent: PropTypes.func.isRequired,\n getConfigs: PropTypes.func.isRequired,\n authActions: PropTypes.object,\n authSelectors: PropTypes.object,\n }\n\n static defaultProps = {\n operationProps: null,\n specPath: List(),\n summary: \"\"\n }\n\n render() {\n\n let {\n isShown,\n toggleShown,\n getComponent,\n authActions,\n authSelectors,\n operationProps,\n specPath,\n } = this.props\n\n let {\n summary,\n isAuthorized,\n method,\n op,\n showSummary,\n path,\n operationId,\n originalOperationId,\n displayOperationId,\n } = operationProps.toJS()\n\n let {\n summary: resolvedSummary,\n } = op\n\n let security = operationProps.get(\"security\")\n\n const AuthorizeOperationBtn = getComponent(\"authorizeOperationBtn\")\n const OperationSummaryMethod = getComponent(\"OperationSummaryMethod\")\n const OperationSummaryPath = getComponent(\"OperationSummaryPath\")\n const JumpToPath = getComponent(\"JumpToPath\", true)\n const CopyToClipboardBtn = getComponent(\"CopyToClipboardBtn\", true)\n\n const hasSecurity = security && !!security.count()\n const securityIsOptional = hasSecurity && security.size === 1 && security.first().isEmpty()\n const allowAnonymous = !hasSecurity || securityIsOptional\n return (\n
    \n \n \n \n\n {!showSummary ? null :\n
    \n {toString(resolvedSummary || summary)}\n
    \n }\n\n {displayOperationId && (originalOperationId || operationId) ? {originalOperationId || operationId} : null}\n\n \n \n \n \n\n {\n allowAnonymous ? null :\n {\n const applicableDefinitions = authSelectors.definitionsForRequirements(security)\n authActions.showDefinitions(applicableDefinitions)\n }}\n />\n }\n \n {/* TODO: use wrapComponents here, swagger-ui doesn't care about jumpToPath */}\n
    \n )\n\n }\n}\n","import React, { PureComponent } from \"react\"\nimport PropTypes from \"prop-types\"\nimport { Iterable } from \"immutable\"\n\nexport default class OperationSummaryMethod extends PureComponent {\n\n static propTypes = {\n operationProps: PropTypes.instanceOf(Iterable).isRequired,\n method: PropTypes.string.isRequired,\n }\n\n static defaultProps = {\n operationProps: null,\n }\n render() {\n\n let {\n method,\n } = this.props\n\n return (\n {method.toUpperCase()}\n )\n }\n}\n","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_instance_splice_d746fc5e__[\"default\"] });","import React, { PureComponent } from \"react\"\nimport PropTypes from \"prop-types\"\nimport { Iterable } from \"immutable\"\nimport { createDeepLinkPath } from \"core/utils\"\nimport ImPropTypes from \"react-immutable-proptypes\"\n\nexport default class OperationSummaryPath extends PureComponent{\n\n static propTypes = {\n specPath: ImPropTypes.list.isRequired,\n operationProps: PropTypes.instanceOf(Iterable).isRequired,\n getComponent: PropTypes.func.isRequired,\n }\n\n render(){\n let {\n getComponent,\n operationProps,\n } = this.props\n\n\n let {\n deprecated,\n isShown,\n path,\n tag,\n operationId,\n isDeepLinkingEnabled,\n } = operationProps.toJS()\n\n /**\n * Add word-break elements between each segment, before the slash\n * to allow browsers an opportunity to break long paths into sensible segments.\n */\n const pathParts = path.split(/(?=\\/)/g)\n for (let i = 1; i < pathParts.length; i += 2) {\n pathParts.splice(i, 0, )\n }\n\n const DeepLink = getComponent( \"DeepLink\" )\n\n return(\n \n \n \n\n )\n }\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\n\nexport const OperationExt = ({ extensions, getComponent }) => {\n let OperationExtRow = getComponent(\"OperationExtRow\")\n return (\n
    \n
    \n

    Extensions

    \n
    \n
    \n\n \n \n \n \n \n \n \n \n {\n extensions.entrySeq().map(([k, v]) => )\n }\n \n
    FieldValue
    \n
    \n
    \n )\n}\nOperationExt.propTypes = {\n extensions: PropTypes.object.isRequired,\n getComponent: PropTypes.func.isRequired\n}\n\nexport default OperationExt\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\n\nexport const OperationExtRow = ({ xKey, xVal }) => {\n const xNormalizedValue = !xVal ? null : xVal.toJS ? xVal.toJS() : xVal\n\n return (\n { xKey }\n { JSON.stringify(xNormalizedValue) }\n )\n}\nOperationExtRow.propTypes = {\n xKey: PropTypes.string,\n xVal: PropTypes.any\n}\n\nexport default OperationExtRow\n","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_js_file_download_bd23dbb6__[\"default\"] });","import React, { useRef, useEffect } from \"react\"\nimport PropTypes from \"prop-types\"\nimport cx from \"classnames\"\nimport {SyntaxHighlighter, getStyle} from \"core/syntax-highlighting\"\nimport get from \"lodash/get\"\nimport isFunction from \"lodash/isFunction\"\nimport saveAs from \"js-file-download\"\nimport { CopyToClipboard } from \"react-copy-to-clipboard\"\n\nconst HighlightCode = ({value, fileName, className, downloadable, getConfigs, canCopy, language}) => {\n const config = isFunction(getConfigs) ? getConfigs() : null\n const canSyntaxHighlight = get(config, \"syntaxHighlight\") !== false && get(config, \"syntaxHighlight.activated\", true)\n const rootRef = useRef(null)\n\n useEffect(() => {\n const childNodes = Array\n .from(rootRef.current.childNodes)\n .filter(node => !!node.nodeType && node.classList.contains(\"microlight\"))\n\n // eslint-disable-next-line no-use-before-define\n childNodes.forEach(node => node.addEventListener(\"mousewheel\", handlePreventYScrollingBeyondElement, { passive: false }))\n\n return () => {\n // eslint-disable-next-line no-use-before-define\n childNodes.forEach(node => node.removeEventListener(\"mousewheel\", handlePreventYScrollingBeyondElement))\n }\n }, [value, className, language])\n\n const handleDownload = () => {\n saveAs(value, fileName)\n }\n\n const handlePreventYScrollingBeyondElement = (e) => {\n const { target, deltaY } = e\n const { scrollHeight: contentHeight, offsetHeight: visibleHeight, scrollTop } = target\n const scrollOffset = visibleHeight + scrollTop\n const isElementScrollable = contentHeight > visibleHeight\n const isScrollingPastTop = scrollTop === 0 && deltaY < 0\n const isScrollingPastBottom = scrollOffset >= contentHeight && deltaY > 0\n\n if (isElementScrollable && (isScrollingPastTop || isScrollingPastBottom)) {\n e.preventDefault()\n }\n }\n\n return (\n
    \n {!downloadable ? null :\n
    \n Download\n
    \n }\n\n {canCopy && (\n
    \n
    \n )}\n\n {canSyntaxHighlight\n ? \n {value}\n \n :
    {value}
    \n }\n\n
    \n )\n}\n\nHighlightCode.propTypes = {\n value: PropTypes.string.isRequired,\n getConfigs: PropTypes.func.isRequired,\n className: PropTypes.string,\n downloadable: PropTypes.bool,\n fileName: PropTypes.string,\n language: PropTypes.string,\n canCopy: PropTypes.bool\n}\n\nHighlightCode.defaultProps = {\n fileName: \"response.txt\"\n}\n\nexport default HighlightCode\n","import React from \"react\"\nimport { fromJS, Iterable } from \"immutable\"\nimport PropTypes from \"prop-types\"\nimport ImPropTypes from \"react-immutable-proptypes\"\nimport { defaultStatusCode, getAcceptControllingResponse } from \"core/utils\"\nimport createHtmlReadyId from \"../../helpers/create-html-ready-id\"\n\nexport default class Responses extends React.Component {\n static propTypes = {\n tryItOutResponse: PropTypes.instanceOf(Iterable),\n responses: PropTypes.instanceOf(Iterable).isRequired,\n produces: PropTypes.instanceOf(Iterable),\n producesValue: PropTypes.any,\n displayRequestDuration: PropTypes.bool.isRequired,\n path: PropTypes.string.isRequired,\n method: PropTypes.string.isRequired,\n getComponent: PropTypes.func.isRequired,\n getConfigs: PropTypes.func.isRequired,\n specSelectors: PropTypes.object.isRequired,\n specActions: PropTypes.object.isRequired,\n oas3Actions: PropTypes.object.isRequired,\n oas3Selectors: PropTypes.object.isRequired,\n specPath: ImPropTypes.list.isRequired,\n fn: PropTypes.object.isRequired\n }\n\n static defaultProps = {\n tryItOutResponse: null,\n produces: fromJS([\"application/json\"]),\n displayRequestDuration: false\n }\n\n // These performance-enhancing checks were disabled as part of Multiple Examples\n // because they were causing data-consistency issues\n //\n // shouldComponentUpdate(nextProps) {\n // // BUG: props.tryItOutResponse is always coming back as a new Immutable instance\n // let render = this.props.tryItOutResponse !== nextProps.tryItOutResponse\n // || this.props.responses !== nextProps.responses\n // || this.props.produces !== nextProps.produces\n // || this.props.producesValue !== nextProps.producesValue\n // || this.props.displayRequestDuration !== nextProps.displayRequestDuration\n // || this.props.path !== nextProps.path\n // || this.props.method !== nextProps.method\n // return render\n // }\n\n\tonChangeProducesWrapper = ( val ) => this.props.specActions.changeProducesValue([this.props.path, this.props.method], val)\n\n onResponseContentTypeChange = ({ controlsAcceptHeader, value }) => {\n const { oas3Actions, path, method } = this.props\n if(controlsAcceptHeader) {\n oas3Actions.setResponseContentType({\n value,\n path,\n method\n })\n }\n }\n\n render() {\n let {\n responses,\n tryItOutResponse,\n getComponent,\n getConfigs,\n specSelectors,\n fn,\n producesValue,\n displayRequestDuration,\n specPath,\n path,\n method,\n oas3Selectors,\n oas3Actions,\n } = this.props\n let defaultCode = defaultStatusCode( responses )\n\n const ContentType = getComponent( \"contentType\" )\n const LiveResponse = getComponent( \"liveResponse\" )\n const Response = getComponent( \"response\" )\n\n let produces = this.props.produces && this.props.produces.size ? this.props.produces : Responses.defaultProps.produces\n\n const isSpecOAS3 = specSelectors.isOAS3()\n\n const acceptControllingResponse = isSpecOAS3 ?\n getAcceptControllingResponse(responses) : null\n\n const regionId = createHtmlReadyId(`${method}${path}_responses`)\n const controlId = `${regionId}_select`\n\n return (\n
    \n
    \n

    Responses

    \n { specSelectors.isOAS3() ? null : }\n
    \n
    \n {\n !tryItOutResponse ? null\n :
    \n \n

    Responses

    \n
    \n\n }\n\n \n \n \n \n \n { specSelectors.isOAS3() ? : null }\n \n \n \n {\n responses.entrySeq().map( ([code, response]) => {\n\n let className = tryItOutResponse && tryItOutResponse.get(\"status\") == code ? \"response_current\" : \"\"\n return (\n \n )\n }).toArray()\n }\n \n
    CodeDescriptionLinks
    \n
    \n
    \n )\n }\n}\n","/**\n * Replace invalid characters from a string to create an html-ready ID\n *\n * @param {string} id A string that may contain invalid characters for the HTML ID attribute\n * @param {string} [replacement=_] The string to replace invalid characters with; \"_\" by default\n * @return {string} Information about the parameter schema\n */\nexport default function createHtmlReadyId(id, replacement = \"_\") {\n return id.replace(/[^\\w-]/g, replacement)\n}\n","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE__babel_runtime_corejs3_core_js_stable_instance_values_a68750d2__[\"default\"] });","import React from \"react\"\nimport PropTypes from \"prop-types\"\nimport ImPropTypes from \"react-immutable-proptypes\"\nimport cx from \"classnames\"\nimport { fromJS, Seq, Iterable, List, Map } from \"immutable\"\nimport { getExtensions, fromJSOrdered, stringify } from \"core/utils\"\nimport { getKnownSyntaxHighlighterLanguage } from \"core/utils/jsonParse\"\n\n\nconst getExampleComponent = ( sampleResponse, HighlightCode, getConfigs ) => {\n if (\n sampleResponse !== undefined &&\n sampleResponse !== null\n ) {\n let language = null\n let testValueForJson = getKnownSyntaxHighlighterLanguage(sampleResponse)\n if (testValueForJson) {\n language = \"json\"\n }\n return
    \n \n
    \n }\n return null\n}\n\nexport default class Response extends React.Component {\n constructor(props, context) {\n super(props, context)\n\n this.state = {\n responseContentType: \"\",\n }\n }\n\n static propTypes = {\n path: PropTypes.string.isRequired,\n method: PropTypes.string.isRequired,\n code: PropTypes.string.isRequired,\n response: PropTypes.instanceOf(Iterable),\n className: PropTypes.string,\n getComponent: PropTypes.func.isRequired,\n getConfigs: PropTypes.func.isRequired,\n specSelectors: PropTypes.object.isRequired,\n oas3Actions: PropTypes.object.isRequired,\n specPath: ImPropTypes.list.isRequired,\n fn: PropTypes.object.isRequired,\n contentType: PropTypes.string,\n activeExamplesKey: PropTypes.string,\n controlsAcceptHeader: PropTypes.bool,\n onContentTypeChange: PropTypes.func\n }\n\n static defaultProps = {\n response: fromJS({}),\n onContentTypeChange: () => {}\n }\n\n _onContentTypeChange = (value) => {\n const { onContentTypeChange, controlsAcceptHeader } = this.props\n this.setState({ responseContentType: value })\n onContentTypeChange({\n value: value,\n controlsAcceptHeader\n })\n }\n\n getTargetExamplesKey = () => {\n const { response, contentType, activeExamplesKey } = this.props\n\n const activeContentType = this.state.responseContentType || contentType\n const activeMediaType = response.getIn([\"content\", activeContentType], Map({}))\n const examplesForMediaType = activeMediaType.get(\"examples\", null)\n\n const firstExamplesKey = examplesForMediaType.keySeq().first()\n return activeExamplesKey || firstExamplesKey\n }\n\n render() {\n let {\n path,\n method,\n code,\n response,\n className,\n specPath,\n fn,\n getComponent,\n getConfigs,\n specSelectors,\n contentType,\n controlsAcceptHeader,\n oas3Actions,\n } = this.props\n\n let { inferSchema, getSampleSchema } = fn\n let isOAS3 = specSelectors.isOAS3()\n const { showExtensions } = getConfigs()\n\n let extensions = showExtensions ? getExtensions(response) : null\n let headers = response.get(\"headers\")\n let links = response.get(\"links\")\n const ResponseExtension = getComponent(\"ResponseExtension\")\n const Headers = getComponent(\"headers\")\n const HighlightCode = getComponent(\"highlightCode\")\n const ModelExample = getComponent(\"modelExample\")\n const Markdown = getComponent(\"Markdown\", true)\n const OperationLink = getComponent(\"operationLink\")\n const ContentType = getComponent(\"contentType\")\n const ExamplesSelect = getComponent(\"ExamplesSelect\")\n const Example = getComponent(\"Example\")\n\n\n var schema, specPathWithPossibleSchema\n\n const activeContentType = this.state.responseContentType || contentType\n const activeMediaType = response.getIn([\"content\", activeContentType], Map({}))\n const examplesForMediaType = activeMediaType.get(\"examples\", null)\n\n // Goal: find a schema value for `schema`\n if(isOAS3) {\n const oas3SchemaForContentType = activeMediaType.get(\"schema\")\n\n schema = oas3SchemaForContentType ? inferSchema(oas3SchemaForContentType.toJS()) : null\n specPathWithPossibleSchema = oas3SchemaForContentType ? List([\"content\", this.state.responseContentType, \"schema\"]) : specPath\n } else {\n schema = response.get(\"schema\")\n specPathWithPossibleSchema = response.has(\"schema\") ? specPath.push(\"schema\") : specPath\n }\n\n let mediaTypeExample\n let shouldOverrideSchemaExample = false\n let sampleSchema\n let sampleGenConfig = {\n includeReadOnly: true\n }\n\n // Goal: find an example value for `sampleResponse`\n if(isOAS3) {\n sampleSchema = activeMediaType.get(\"schema\")?.toJS()\n if(examplesForMediaType) {\n const targetExamplesKey = this.getTargetExamplesKey()\n const targetExample = examplesForMediaType\n .get(targetExamplesKey, Map({}))\n const getMediaTypeExample = (targetExample) =>\n targetExample.get(\"value\")\n mediaTypeExample = getMediaTypeExample(targetExample)\n if(mediaTypeExample === undefined) {\n mediaTypeExample = getMediaTypeExample(examplesForMediaType.values().next().value)\n }\n shouldOverrideSchemaExample = true\n } else if(activeMediaType.get(\"example\") !== undefined) {\n // use the example key's value\n mediaTypeExample = activeMediaType.get(\"example\")\n shouldOverrideSchemaExample = true\n }\n } else {\n sampleSchema = schema\n sampleGenConfig = {...sampleGenConfig, includeWriteOnly: true}\n const oldOASMediaTypeExample = response.getIn([\"examples\", activeContentType])\n if(oldOASMediaTypeExample) {\n mediaTypeExample = oldOASMediaTypeExample\n shouldOverrideSchemaExample = true\n }\n }\n\n const sampleResponse = getSampleSchema(\n sampleSchema,\n activeContentType,\n sampleGenConfig,\n shouldOverrideSchemaExample ? mediaTypeExample : undefined\n )\n\n let example = getExampleComponent( sampleResponse, HighlightCode, getConfigs )\n\n return (\n \n \n { code }\n \n \n\n
    \n \n
    \n\n { !showExtensions || !extensions.size ? null : extensions.entrySeq().map(([key, v]) => )}\n\n {isOAS3 && response.get(\"content\") ? (\n
    \n \n \n Media type\n \n \n {controlsAcceptHeader ? (\n \n Controls Accept header.\n \n ) : null}\n \n {examplesForMediaType ? (\n
    \n \n Examples\n \n \n oas3Actions.setActiveExamplesMember({\n name: key,\n pathMethod: [path, method],\n contextType: \"responses\",\n contextName: code\n })\n }\n showLabels={false}\n />\n
    \n ) : null}\n
    \n ) : null}\n\n { example || schema ? (\n \n ) : null }\n\n { isOAS3 && examplesForMediaType ? (\n \n ) : null}\n\n { headers ? (\n \n ) : null}\n\n \n {isOAS3 ? \n { links ?\n links.toSeq().entrySeq().map(([key, link]) => {\n return \n })\n : No links}\n : null}\n \n )\n }\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\n\nexport const ResponseExtension = ({ xKey, xVal }) => {\n return
    { xKey }: { String(xVal) }
    \n}\nResponseExtension.propTypes = {\n xKey: PropTypes.string,\n xVal: PropTypes.any\n}\n\nexport default ResponseExtension\n","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_xml_but_prettier_2ed4d5cb__[\"default\"] });","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_lodash_toLower_c29ee2b0__[\"default\"] });","import React from \"react\"\nimport PropTypes from \"prop-types\"\nimport formatXml from \"xml-but-prettier\"\nimport toLower from \"lodash/toLower\"\nimport { extractFileNameFromContentDispositionHeader } from \"core/utils\"\nimport { getKnownSyntaxHighlighterLanguage } from \"core/utils/jsonParse\"\nimport win from \"core/window\"\n\nexport default class ResponseBody extends React.PureComponent {\n state = {\n parsedContent: null\n }\n\n static propTypes = {\n content: PropTypes.any.isRequired,\n contentType: PropTypes.string,\n getConfigs: PropTypes.func.isRequired,\n getComponent: PropTypes.func.isRequired,\n headers: PropTypes.object,\n url: PropTypes.string\n }\n\n updateParsedContent = (prevContent) => {\n const { content } = this.props\n\n if(prevContent === content) {\n return\n }\n\n if(content && content instanceof Blob) {\n var reader = new FileReader()\n reader.onload = () => {\n this.setState({\n parsedContent: reader.result\n })\n }\n reader.readAsText(content)\n } else {\n this.setState({\n parsedContent: content.toString()\n })\n }\n }\n\n componentDidMount() {\n this.updateParsedContent(null)\n }\n\n componentDidUpdate(prevProps) {\n this.updateParsedContent(prevProps.content)\n }\n\n render() {\n let { content, contentType, url, headers={}, getConfigs, getComponent } = this.props\n const { parsedContent } = this.state\n const HighlightCode = getComponent(\"highlightCode\")\n const downloadName = \"response_\" + new Date().getTime()\n let body, bodyEl\n url = url || \"\"\n\n if (\n (/^application\\/octet-stream/i.test(contentType) ||\n (headers[\"Content-Disposition\"] && /attachment/i.test(headers[\"Content-Disposition\"])) ||\n (headers[\"content-disposition\"] && /attachment/i.test(headers[\"content-disposition\"])) ||\n (headers[\"Content-Description\"] && /File Transfer/i.test(headers[\"Content-Description\"])) ||\n (headers[\"content-description\"] && /File Transfer/i.test(headers[\"content-description\"]))) &&\n content.size > 0\n ) {\n // Download\n\n if (\"Blob\" in window) {\n let type = contentType || \"text/html\"\n let blob = (content instanceof Blob) ? content : new Blob([content], {type: type})\n let href = window.URL.createObjectURL(blob)\n let fileName = url.substr(url.lastIndexOf(\"/\") + 1)\n let download = [type, fileName, href].join(\":\")\n\n // Use filename from response header,\n // First check if filename is quoted (e.g. contains space), if no, fallback to not quoted check\n let disposition = headers[\"content-disposition\"] || headers[\"Content-Disposition\"]\n if (typeof disposition !== \"undefined\") {\n let responseFilename = extractFileNameFromContentDispositionHeader(disposition)\n if (responseFilename !== null) {\n download = responseFilename\n }\n }\n\n if(win.navigator && win.navigator.msSaveOrOpenBlob) {\n bodyEl = \n } else {\n bodyEl = \n }\n } else {\n bodyEl =
    Download headers detected but your browser does not support downloading binary via XHR (Blob).
    \n }\n\n // Anything else (CORS)\n } else if (/json/i.test(contentType)) {\n // JSON\n let language = null\n let testValueForJson = getKnownSyntaxHighlighterLanguage(content)\n if (testValueForJson) {\n language = \"json\"\n }\n try {\n body = JSON.stringify(JSON.parse(content), null, \" \")\n } catch (error) {\n body = \"can't parse JSON. Raw result:\\n\\n\" + content\n }\n\n bodyEl = \n\n // XML\n } else if (/xml/i.test(contentType)) {\n body = formatXml(content, {\n textNodesOnSameLine: true,\n indentor: \" \"\n })\n bodyEl = \n\n // HTML or Plain Text\n } else if (toLower(contentType) === \"text/html\" || /text\\/plain/.test(contentType)) {\n bodyEl = \n\n // CSV\n } else if (toLower(contentType) === \"text/csv\" || /text\\/csv/.test(contentType)) {\n bodyEl = \n\n // Image\n } else if (/^image\\//i.test(contentType)) {\n if(contentType.includes(\"svg\")) {\n bodyEl =
    { content }
    \n } else {\n bodyEl = \n }\n\n // Audio\n } else if (/^audio\\//i.test(contentType)) {\n bodyEl =
    \n } else if (typeof content === \"string\") {\n bodyEl = \n } else if ( content.size > 0 ) {\n // We don't know the contentType, but there was some content returned\n if(parsedContent) {\n // We were able to squeeze something out of content\n // in `updateParsedContent`, so let's display it\n bodyEl =
    \n

    \n Unrecognized response type; displaying content as text.\n

    \n \n
    \n\n } else {\n // Give up\n bodyEl =

    \n Unrecognized response type; unable to display.\n

    \n }\n } else {\n // We don't know the contentType and there was no content returned\n bodyEl = null\n }\n\n return ( !bodyEl ? null :
    \n
    Response body
    \n { bodyEl }\n
    \n )\n }\n}\n","import React, { Component } from \"react\"\nimport PropTypes from \"prop-types\"\nimport { Map, List } from \"immutable\"\nimport ImPropTypes from \"react-immutable-proptypes\"\n\nexport default class Parameters extends Component {\n\n constructor(props) {\n super(props)\n this.state = {\n callbackVisible: false,\n parametersVisible: true,\n }\n }\n\n static propTypes = {\n parameters: ImPropTypes.list.isRequired,\n operation: PropTypes.object.isRequired,\n specActions: PropTypes.object.isRequired,\n getComponent: PropTypes.func.isRequired,\n specSelectors: PropTypes.object.isRequired,\n oas3Actions: PropTypes.object.isRequired,\n oas3Selectors: PropTypes.object.isRequired,\n fn: PropTypes.object.isRequired,\n tryItOutEnabled: PropTypes.bool,\n allowTryItOut: PropTypes.bool,\n onTryoutClick: PropTypes.func,\n onResetClick: PropTypes.func,\n onCancelClick: PropTypes.func,\n onChangeKey: PropTypes.array,\n pathMethod: PropTypes.array.isRequired,\n getConfigs: PropTypes.func.isRequired,\n specPath: ImPropTypes.list.isRequired,\n }\n\n\n static defaultProps = {\n onTryoutClick: Function.prototype,\n onCancelClick: Function.prototype,\n tryItOutEnabled: false,\n allowTryItOut: true,\n onChangeKey: [],\n specPath: [],\n }\n\n onChange = (param, value, isXml) => {\n let {\n specActions: { changeParamByIdentity },\n onChangeKey,\n } = this.props\n\n changeParamByIdentity(onChangeKey, param, value, isXml)\n }\n\n onChangeConsumesWrapper = (val) => {\n let {\n specActions: { changeConsumesValue },\n onChangeKey,\n } = this.props\n\n changeConsumesValue(onChangeKey, val)\n }\n\n toggleTab = (tab) => {\n if (tab === \"parameters\") {\n return this.setState({\n parametersVisible: true,\n callbackVisible: false,\n })\n } else if (tab === \"callbacks\") {\n return this.setState({\n callbackVisible: true,\n parametersVisible: false,\n })\n }\n }\n \n onChangeMediaType = ({ value, pathMethod }) => {\n let { specActions, oas3Selectors, oas3Actions } = this.props\n const userHasEditedBody = oas3Selectors.hasUserEditedBody(...pathMethod)\n const shouldRetainRequestBodyValue = oas3Selectors.shouldRetainRequestBodyValue(...pathMethod)\n oas3Actions.setRequestContentType({ value, pathMethod })\n oas3Actions.initRequestBodyValidateError({ pathMethod })\n if (!userHasEditedBody) {\n if(!shouldRetainRequestBodyValue) {\n oas3Actions.setRequestBodyValue({ value: undefined, pathMethod })\n }\n specActions.clearResponse(...pathMethod)\n specActions.clearRequest(...pathMethod)\n specActions.clearValidateParams(pathMethod)\n }\n }\n\n render() {\n\n let {\n onTryoutClick,\n onResetClick,\n parameters,\n allowTryItOut,\n tryItOutEnabled,\n specPath,\n fn,\n getComponent,\n getConfigs,\n specSelectors,\n specActions,\n pathMethod,\n oas3Actions,\n oas3Selectors,\n operation,\n } = this.props\n\n const ParameterRow = getComponent(\"parameterRow\")\n const TryItOutButton = getComponent(\"TryItOutButton\")\n const ContentType = getComponent(\"contentType\")\n const Callbacks = getComponent(\"Callbacks\", true)\n const RequestBody = getComponent(\"RequestBody\", true)\n\n const isExecute = tryItOutEnabled && allowTryItOut\n const isOAS3 = specSelectors.isOAS3()\n\n\n const requestBody = operation.get(\"requestBody\")\n\n const groupedParametersArr = Object.values(parameters\n .reduce((acc, x) => {\n const key = x.get(\"in\")\n acc[key] ??= []\n acc[key].push(x)\n return acc\n }, {}))\n .reduce((acc, x) => acc.concat(x), [])\n\n const retainRequestBodyValueFlagForOperation = (f) => oas3Actions.setRetainRequestBodyValueFlag({ value: f, pathMethod })\n return (\n
    \n
    \n {isOAS3 ? (\n
    \n
    this.toggleTab(\"parameters\")}\n className={`tab-item ${this.state.parametersVisible && \"active\"}`}>\n

    Parameters

    \n
    \n {operation.get(\"callbacks\") ?\n (\n
    this.toggleTab(\"callbacks\")}\n className={`tab-item ${this.state.callbackVisible && \"active\"}`}>\n

    Callbacks

    \n
    \n ) : null\n }\n
    \n ) : (\n
    \n

    Parameters

    \n
    \n )}\n {allowTryItOut ? (\n onResetClick(pathMethod)}/>\n ) : null}\n
    \n {this.state.parametersVisible ?
    \n {!groupedParametersArr.length ?

    No parameters

    :\n
    \n \n \n \n \n \n \n \n \n {\n groupedParametersArr.map((parameter, i) => (\n \n ))\n }\n \n
    NameDescription
    \n
    \n }\n
    : null}\n\n {this.state.callbackVisible ?
    \n \n
    : null}\n {\n isOAS3 && requestBody && this.state.parametersVisible &&\n
    \n
    \n

    Request\n body

    \n \n
    \n
    \n {\n this.props.oas3Actions.setActiveExamplesMember({\n name: key,\n pathMethod: this.props.pathMethod,\n contextType: \"requestBody\",\n contextName: \"requestBody\", // RBs are currently not stored per-mediaType\n })\n }\n }\n onChange={(value, path) => {\n if (path) {\n const lastValue = oas3Selectors.requestBodyValue(...pathMethod)\n const usableValue = Map.isMap(lastValue) ? lastValue : Map()\n return oas3Actions.setRequestBodyValue({\n pathMethod,\n value: usableValue.setIn(path, value),\n })\n }\n oas3Actions.setRequestBodyValue({ value, pathMethod })\n }}\n onChangeIncludeEmpty={(name, value) => {\n oas3Actions.setRequestBodyInclusion({\n pathMethod,\n value,\n name,\n })\n }}\n contentType={oas3Selectors.requestContentType(...pathMethod)} />\n
    \n
    \n }\n
    \n )\n }\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\n\nexport const ParameterExt = ({ xKey, xVal }) => {\n return
    { xKey }: { String(xVal) }
    \n}\nParameterExt.propTypes = {\n xKey: PropTypes.string,\n xVal: PropTypes.any\n}\n\nexport default ParameterExt\n","import React, { Component } from \"react\"\nimport cx from \"classnames\"\nimport PropTypes from \"prop-types\"\n\n\nconst noop = () => { }\n\nconst ParameterIncludeEmptyPropTypes = {\n isIncluded: PropTypes.bool.isRequired,\n isDisabled: PropTypes.bool.isRequired,\n isIncludedOptions: PropTypes.object,\n onChange: PropTypes.func.isRequired,\n}\n\nconst ParameterIncludeEmptyDefaultProps = {\n onChange: noop,\n isIncludedOptions: {},\n}\nexport default class ParameterIncludeEmpty extends Component {\n static propTypes = ParameterIncludeEmptyPropTypes\n static defaultProps = ParameterIncludeEmptyDefaultProps\n\n componentDidMount() {\n const { isIncludedOptions, onChange } = this.props\n const { shouldDispatchInit, defaultValue } = isIncludedOptions\n if (shouldDispatchInit) {\n onChange(defaultValue)\n }\n }\n\n onCheckboxChange = e => {\n const { onChange } = this.props\n onChange(e.target.checked)\n }\n\n render() {\n let { isIncluded, isDisabled } = this.props\n\n return (\n
    \n \n
    \n )\n }\n}\n","import React, { Component } from \"react\"\nimport { Map, List } from \"immutable\"\nimport PropTypes from \"prop-types\"\nimport ImPropTypes from \"react-immutable-proptypes\"\nimport win from \"core/window\"\nimport { getExtensions, getCommonExtensions, numberToString, stringify, isEmptyValue } from \"core/utils\"\nimport getParameterSchema from \"../../helpers/get-parameter-schema.js\"\n\nexport default class ParameterRow extends Component {\n static propTypes = {\n onChange: PropTypes.func.isRequired,\n param: PropTypes.object.isRequired,\n rawParam: PropTypes.object.isRequired,\n getComponent: PropTypes.func.isRequired,\n fn: PropTypes.object.isRequired,\n isExecute: PropTypes.bool,\n onChangeConsumes: PropTypes.func.isRequired,\n specSelectors: PropTypes.object.isRequired,\n specActions: PropTypes.object.isRequired,\n pathMethod: PropTypes.array.isRequired,\n getConfigs: PropTypes.func.isRequired,\n specPath: ImPropTypes.list.isRequired,\n oas3Actions: PropTypes.object.isRequired,\n oas3Selectors: PropTypes.object.isRequired,\n }\n\n constructor(props, context) {\n super(props, context)\n\n this.setDefaultValue()\n }\n\n UNSAFE_componentWillReceiveProps(props) {\n let { specSelectors, pathMethod, rawParam } = props\n let isOAS3 = specSelectors.isOAS3()\n\n let parameterWithMeta = specSelectors.parameterWithMetaByIdentity(pathMethod, rawParam) || new Map()\n // fallback, if the meta lookup fails\n parameterWithMeta = parameterWithMeta.isEmpty() ? rawParam : parameterWithMeta\n\n let enumValue\n\n if(isOAS3) {\n let { schema } = getParameterSchema(parameterWithMeta, { isOAS3 })\n enumValue = schema ? schema.get(\"enum\") : undefined\n } else {\n enumValue = parameterWithMeta ? parameterWithMeta.get(\"enum\") : undefined\n }\n let paramValue = parameterWithMeta ? parameterWithMeta.get(\"value\") : undefined\n\n let value\n\n if ( paramValue !== undefined ) {\n value = paramValue\n } else if ( rawParam.get(\"required\") && enumValue && enumValue.size ) {\n value = enumValue.first()\n }\n\n if ( value !== undefined && value !== paramValue ) {\n this.onChangeWrapper(numberToString(value))\n }\n // todo: could check if schema here; if not, do not call. impact?\n this.setDefaultValue()\n }\n\n onChangeWrapper = (value, isXml = false) => {\n let { onChange, rawParam } = this.props\n let valueForUpstream\n\n // Coerce empty strings and empty Immutable objects to null\n if(value === \"\" || (value && value.size === 0)) {\n valueForUpstream = null\n } else {\n valueForUpstream = value\n }\n\n return onChange(rawParam, valueForUpstream, isXml)\n }\n\n _onExampleSelect = (key, /* { isSyntheticChange } = {} */) => {\n this.props.oas3Actions.setActiveExamplesMember({\n name: key,\n pathMethod: this.props.pathMethod,\n contextType: \"parameters\",\n contextName: this.getParamKey()\n })\n }\n\n onChangeIncludeEmpty = (newValue) => {\n let { specActions, param, pathMethod } = this.props\n const paramName = param.get(\"name\")\n const paramIn = param.get(\"in\")\n return specActions.updateEmptyParamInclusion(pathMethod, paramName, paramIn, newValue)\n }\n\n setDefaultValue = () => {\n let { specSelectors, pathMethod, rawParam, oas3Selectors, fn } = this.props\n\n const paramWithMeta = specSelectors.parameterWithMetaByIdentity(pathMethod, rawParam) || Map()\n const { schema } = getParameterSchema(paramWithMeta, { isOAS3: specSelectors.isOAS3() })\n const parameterMediaType = paramWithMeta\n .get(\"content\", Map())\n .keySeq()\n .first()\n\n // getSampleSchema could return null\n const generatedSampleValue = schema ? fn.getSampleSchema(schema.toJS(), parameterMediaType, {\n\n includeWriteOnly: true\n }) : null\n\n if (!paramWithMeta || paramWithMeta.get(\"value\") !== undefined) {\n return\n }\n\n if( paramWithMeta.get(\"in\") !== \"body\" ) {\n let initialValue\n\n //// Find an initial value\n\n if (specSelectors.isSwagger2()) {\n initialValue =\n paramWithMeta.get(\"x-example\") !== undefined\n ? paramWithMeta.get(\"x-example\")\n : paramWithMeta.getIn([\"schema\", \"example\"]) !== undefined\n ? paramWithMeta.getIn([\"schema\", \"example\"])\n : (schema && schema.getIn([\"default\"]))\n } else if (specSelectors.isOAS3()) {\n const currentExampleKey = oas3Selectors.activeExamplesMember(...pathMethod, \"parameters\", this.getParamKey())\n initialValue =\n paramWithMeta.getIn([\"examples\", currentExampleKey, \"value\"]) !== undefined\n ? paramWithMeta.getIn([\"examples\", currentExampleKey, \"value\"])\n : paramWithMeta.getIn([\"content\", parameterMediaType, \"example\"]) !== undefined\n ? paramWithMeta.getIn([\"content\", parameterMediaType, \"example\"])\n : paramWithMeta.get(\"example\") !== undefined\n ? paramWithMeta.get(\"example\")\n : (schema && schema.get(\"example\")) !== undefined\n ? (schema && schema.get(\"example\"))\n : (schema && schema.get(\"default\")) !== undefined\n ? (schema && schema.get(\"default\"))\n : paramWithMeta.get(\"default\") // ensures support for `parameterMacro`\n }\n\n //// Process the initial value\n\n if(initialValue !== undefined && !List.isList(initialValue)) {\n // Stringify if it isn't a List\n initialValue = stringify(initialValue)\n }\n\n //// Dispatch the initial value\n\n if(initialValue !== undefined) {\n this.onChangeWrapper(initialValue)\n } else if(\n schema && schema.get(\"type\") === \"object\"\n && generatedSampleValue\n && !paramWithMeta.get(\"examples\")\n ) {\n // Object parameters get special treatment.. if the user doesn't set any\n // default or example values, we'll provide initial values generated from\n // the schema.\n // However, if `examples` exist for the parameter, we won't do anything,\n // so that the appropriate `examples` logic can take over.\n this.onChangeWrapper(\n List.isList(generatedSampleValue) ? (\n generatedSampleValue\n ) : (\n stringify(generatedSampleValue)\n )\n )\n }\n }\n }\n\n getParamKey() {\n const { param } = this.props\n\n if(!param) return null\n\n return `${param.get(\"name\")}-${param.get(\"in\")}`\n }\n\n render() {\n let {param, rawParam, getComponent, getConfigs, isExecute, fn, onChangeConsumes, specSelectors, pathMethod, specPath, oas3Selectors} = this.props\n\n let isOAS3 = specSelectors.isOAS3()\n\n const { showExtensions, showCommonExtensions } = getConfigs()\n\n if(!param) {\n param = rawParam\n }\n\n if(!rawParam) return null\n\n // const onChangeWrapper = (value) => onChange(param, value)\n const JsonSchemaForm = getComponent(\"JsonSchemaForm\")\n const ParamBody = getComponent(\"ParamBody\")\n let inType = param.get(\"in\")\n let bodyParam = inType !== \"body\" ? null\n : \n\n const ModelExample = getComponent(\"modelExample\")\n const Markdown = getComponent(\"Markdown\", true)\n const ParameterExt = getComponent(\"ParameterExt\")\n const ParameterIncludeEmpty = getComponent(\"ParameterIncludeEmpty\")\n const ExamplesSelectValueRetainer = getComponent(\"ExamplesSelectValueRetainer\")\n const Example = getComponent(\"Example\")\n\n let { schema } = getParameterSchema(param, { isOAS3 })\n let paramWithMeta = specSelectors.parameterWithMetaByIdentity(pathMethod, rawParam) || Map()\n\n let format = schema ? schema.get(\"format\") : null\n let type = schema ? schema.get(\"type\") : null\n let itemType = schema ? schema.getIn([\"items\", \"type\"]) : null\n let isFormData = inType === \"formData\"\n let isFormDataSupported = \"FormData\" in win\n let required = param.get(\"required\")\n\n let value = paramWithMeta ? paramWithMeta.get(\"value\") : \"\"\n let commonExt = showCommonExtensions ? getCommonExtensions(schema) : null\n let extensions = showExtensions ? getExtensions(param) : null\n\n let paramItems // undefined\n let paramEnum // undefined\n let paramDefaultValue // undefined\n let paramExample // undefined\n let isDisplayParamEnum = false\n\n if ( param !== undefined && schema ) {\n paramItems = schema.get(\"items\")\n }\n\n if (paramItems !== undefined) {\n paramEnum = paramItems.get(\"enum\")\n paramDefaultValue = paramItems.get(\"default\")\n } else if (schema) {\n paramEnum = schema.get(\"enum\")\n }\n\n if ( paramEnum && paramEnum.size && paramEnum.size > 0) {\n isDisplayParamEnum = true\n }\n\n // Default and Example Value for readonly doc\n if ( param !== undefined ) {\n if (schema) {\n paramDefaultValue = schema.get(\"default\")\n }\n if (paramDefaultValue === undefined) {\n paramDefaultValue = param.get(\"default\")\n }\n paramExample = param.get(\"example\")\n if (paramExample === undefined) {\n paramExample = param.get(\"x-example\")\n }\n }\n\n return (\n \n \n
    \n { param.get(\"name\") }\n { !required ? null :  * }\n
    \n
    \n { type }\n { itemType && `[${itemType}]` }\n { format && (${format})}\n
    \n
    \n { isOAS3 && param.get(\"deprecated\") ? \"deprecated\": null }\n
    \n
    ({ param.get(\"in\") })
    \n { !showCommonExtensions || !commonExt.size ? null : commonExt.entrySeq().map(([key, v]) => )}\n { !showExtensions || !extensions.size ? null : extensions.entrySeq().map(([key, v]) => )}\n \n\n \n { param.get(\"description\") ? : null }\n\n { (bodyParam || !isExecute) && isDisplayParamEnum ?\n Available values : \" + paramEnum.map(function(item) {\n return item\n }).toArray().join(\", \")}/>\n : null\n }\n\n { (bodyParam || !isExecute) && paramDefaultValue !== undefined ?\n Default value : \" + paramDefaultValue}/>\n : null\n }\n\n { (bodyParam || !isExecute) && paramExample !== undefined ?\n Example : \" + paramExample}/>\n : null\n }\n\n {(isFormData && !isFormDataSupported) &&
    Error: your browser does not support FormData
    }\n\n {\n isOAS3 && param.get(\"examples\") ? (\n
    \n \n
    \n ) : null\n }\n\n { bodyParam ? null\n : \n }\n\n\n {\n bodyParam && schema ? \n : null\n }\n\n {\n !bodyParam && isExecute && param.get(\"allowEmptyValue\") ?\n \n : null\n }\n\n {\n isOAS3 && param.get(\"examples\") ? (\n \n ) : null\n }\n\n \n\n \n )\n\n }\n\n}\n","import React, { Component } from \"react\"\nimport PropTypes from \"prop-types\"\n\nexport default class Execute extends Component {\n\n static propTypes = {\n specSelectors: PropTypes.object.isRequired,\n specActions: PropTypes.object.isRequired,\n operation: PropTypes.object.isRequired,\n path: PropTypes.string.isRequired,\n method: PropTypes.string.isRequired,\n oas3Selectors: PropTypes.object.isRequired,\n oas3Actions: PropTypes.object.isRequired,\n onExecute: PropTypes.func,\n disabled: PropTypes.bool\n }\n\n handleValidateParameters = () => {\n let { specSelectors, specActions, path, method } = this.props\n specActions.validateParams([path, method])\n return specSelectors.validateBeforeExecute([path, method])\n }\n\n handleValidateRequestBody = () => {\n let { path, method, specSelectors, oas3Selectors, oas3Actions } = this.props\n let validationErrors = {\n missingBodyValue: false,\n missingRequiredKeys: []\n }\n // context: reset errors, then (re)validate\n oas3Actions.clearRequestBodyValidateError({ path, method })\n let oas3RequiredRequestBodyContentType = specSelectors.getOAS3RequiredRequestBodyContentType([path, method])\n let oas3RequestBodyValue = oas3Selectors.requestBodyValue(path, method)\n let oas3ValidateBeforeExecuteSuccess = oas3Selectors.validateBeforeExecute([path, method])\n let oas3RequestContentType = oas3Selectors.requestContentType(path, method)\n\n if (!oas3ValidateBeforeExecuteSuccess) {\n validationErrors.missingBodyValue = true\n oas3Actions.setRequestBodyValidateError({ path, method, validationErrors })\n return false\n }\n if (!oas3RequiredRequestBodyContentType) {\n return true\n }\n let missingRequiredKeys = oas3Selectors.validateShallowRequired({\n oas3RequiredRequestBodyContentType,\n oas3RequestContentType,\n oas3RequestBodyValue\n })\n if (!missingRequiredKeys || missingRequiredKeys.length < 1) {\n return true\n }\n missingRequiredKeys.forEach((missingKey) => {\n validationErrors.missingRequiredKeys.push(missingKey)\n })\n oas3Actions.setRequestBodyValidateError({ path, method, validationErrors })\n return false\n }\n\n handleValidationResultPass = () => {\n let { specActions, operation, path, method } = this.props\n if (this.props.onExecute) {\n // loading spinner\n this.props.onExecute()\n }\n specActions.execute({ operation, path, method })\n }\n\n handleValidationResultFail = () => {\n let { specActions, path, method } = this.props\n // deferred by 40ms, to give element class change time to settle.\n specActions.clearValidateParams([path, method])\n setTimeout(() => {\n specActions.validateParams([path, method])\n }, 40)\n }\n\n handleValidationResult = (isPass) => {\n if (isPass) {\n this.handleValidationResultPass()\n } else {\n this.handleValidationResultFail()\n }\n }\n\n onClick = () => {\n let paramsResult = this.handleValidateParameters()\n let requestBodyResult = this.handleValidateRequestBody()\n let isPass = paramsResult && requestBodyResult\n this.handleValidationResult(isPass)\n }\n\n onChangeProducesWrapper = ( val ) => this.props.specActions.changeProducesValue([this.props.path, this.props.method], val)\n\n render(){\n const { disabled } = this.props\n return (\n \n )\n }\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\nimport Im from \"immutable\"\n\nconst propClass = \"header-example\"\n\nexport default class Headers extends React.Component {\n static propTypes = {\n headers: PropTypes.object.isRequired,\n getComponent: PropTypes.func.isRequired\n }\n\n render() {\n let { headers, getComponent } = this.props\n\n const Property = getComponent(\"Property\")\n const Markdown = getComponent(\"Markdown\", true)\n\n if ( !headers || !headers.size )\n return null\n\n return (\n
    \n

    Headers:

    \n \n \n \n \n \n \n \n \n \n {\n headers.entrySeq().map( ([ key, header ]) => {\n if(!Im.Map.isMap(header)) {\n return null\n }\n\n const description = header.get(\"description\")\n const type = header.getIn([\"schema\"]) ? header.getIn([\"schema\", \"type\"]) : header.getIn([\"type\"])\n const schemaExample = header.getIn([\"schema\", \"example\"])\n\n return (\n \n \n \n )\n }).toArray()\n }\n \n
    NameDescriptionType
    { key }{\n !description ? null : \n }{ type } { schemaExample ? : null }
    \n
    \n )\n }\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\nimport { List } from \"immutable\"\n\nexport default class Errors extends React.Component {\n\n static propTypes = {\n editorActions: PropTypes.object,\n errSelectors: PropTypes.object.isRequired,\n layoutSelectors: PropTypes.object.isRequired,\n layoutActions: PropTypes.object.isRequired,\n getComponent: PropTypes.func.isRequired,\n }\n\n render() {\n let { editorActions, errSelectors, layoutSelectors, layoutActions, getComponent } = this.props\n\n const Collapse = getComponent(\"Collapse\")\n\n if(editorActions && editorActions.jumpToLine) {\n var jumpToLine = editorActions.jumpToLine\n }\n\n let errors = errSelectors.allErrors()\n\n // all thrown errors, plus error-level everything else\n let allErrorsToDisplay = errors.filter(err => err.get(\"type\") === \"thrown\" ? true :err.get(\"level\") === \"error\")\n\n if(!allErrorsToDisplay || allErrorsToDisplay.count() < 1) {\n return null\n }\n\n let isVisible = layoutSelectors.isShown([\"errorPane\"], true)\n let toggleVisibility = () => layoutActions.show([\"errorPane\"], !isVisible)\n\n let sortedJSErrors = allErrorsToDisplay.sortBy(err => err.get(\"line\"))\n\n return (\n
    \n        
    \n

    Errors

    \n \n
    \n \n
    \n { sortedJSErrors.map((err, i) => {\n let type = err.get(\"type\")\n if(type === \"thrown\" || type === \"auth\") {\n return \n }\n if(type === \"spec\") {\n return \n }\n }) }\n
    \n
    \n
    \n )\n }\n}\n\nconst ThrownErrorItem = ( { error, jumpToLine } ) => {\n if(!error) {\n return null\n }\n let errorLine = error.get(\"line\")\n\n return (\n
    \n { !error ? null :\n
    \n

    { (error.get(\"source\") && error.get(\"level\")) ?\n toTitleCase(error.get(\"source\")) + \" \" + error.get(\"level\") : \"\" }\n { error.get(\"path\") ? at {error.get(\"path\")}: null }

    \n \n { error.get(\"message\") }\n \n
    \n { errorLine && jumpToLine ? Jump to line { errorLine } : null }\n
    \n
    \n }\n
    \n )\n }\n\nconst SpecErrorItem = ( { error, jumpToLine } ) => {\n let locationMessage = null\n\n if(error.get(\"path\")) {\n if(List.isList(error.get(\"path\"))) {\n locationMessage = at { error.get(\"path\").join(\".\") }\n } else {\n locationMessage = at { error.get(\"path\") }\n }\n } else if(error.get(\"line\") && !jumpToLine) {\n locationMessage = on line { error.get(\"line\") }\n }\n\n return (\n
    \n { !error ? null :\n
    \n

    { toTitleCase(error.get(\"source\")) + \" \" + error.get(\"level\") } { locationMessage }

    \n { error.get(\"message\") }\n
    \n { jumpToLine ? (\n Jump to line { error.get(\"line\") }\n ) : null }\n
    \n
    \n }\n
    \n )\n }\n\nfunction toTitleCase(str) {\n return (str || \"\")\n .split(\" \")\n .map(substr => substr[0].toUpperCase() + substr.slice(1))\n .join(\" \")\n}\n\nThrownErrorItem.propTypes = {\n error: PropTypes.object.isRequired,\n jumpToLine: PropTypes.func\n}\n\nThrownErrorItem.defaultProps = {\n jumpToLine: null\n}\n\nSpecErrorItem.propTypes = {\n error: PropTypes.object.isRequired,\n jumpToLine: PropTypes.func\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\nimport ImPropTypes from \"react-immutable-proptypes\"\nimport { fromJS } from \"immutable\"\n\nconst noop = ()=>{}\n\nexport default class ContentType extends React.Component {\n\n static propTypes = {\n ariaControls: PropTypes.string,\n contentTypes: PropTypes.oneOfType([ImPropTypes.list, ImPropTypes.set, ImPropTypes.seq]),\n controlId: PropTypes.string,\n value: PropTypes.string,\n onChange: PropTypes.func,\n className: PropTypes.string,\n ariaLabel: PropTypes.string\n }\n\n static defaultProps = {\n onChange: noop,\n value: null,\n contentTypes: fromJS([\"application/json\"]),\n }\n\n componentDidMount() {\n // Needed to populate the form, initially\n if(this.props.contentTypes) {\n this.props.onChange(this.props.contentTypes.first())\n }\n }\n\n UNSAFE_componentWillReceiveProps(nextProps) {\n if(!nextProps.contentTypes || !nextProps.contentTypes.size) {\n return\n }\n\n if(!nextProps.contentTypes.includes(nextProps.value)) {\n nextProps.onChange(nextProps.contentTypes.first())\n }\n }\n\n onChangeWrapper = e => this.props.onChange(e.target.value)\n\n render() {\n let { ariaControls, ariaLabel, className, contentTypes, controlId, value } = this.props\n\n if ( !contentTypes || !contentTypes.size )\n return null\n\n return (\n
    \n \n
    \n )\n }\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\n\nfunction xclass(...args) {\n return args.filter(a => !!a).join(\" \").trim()\n}\n\nexport class Container extends React.Component {\n render() {\n let { fullscreen, full, ...rest } = this.props\n // Normal element\n\n if(fullscreen)\n return
    \n\n let containerClass = \"swagger-container\" + (full ? \"-full\" : \"\")\n return (\n
    \n )\n }\n}\n\nContainer.propTypes = {\n fullscreen: PropTypes.bool,\n full: PropTypes.bool,\n className: PropTypes.string\n}\n\nconst DEVICES = {\n \"mobile\": \"\",\n \"tablet\": \"-tablet\",\n \"desktop\": \"-desktop\",\n \"large\": \"-hd\"\n}\n\nexport class Col extends React.Component {\n\n render() {\n const {\n hide,\n keepContents,\n /* we don't want these in the `rest` object that passes to the final component,\n since React now complains. So we extract them */\n /* eslint-disable no-unused-vars */\n mobile,\n tablet,\n desktop,\n large,\n /* eslint-enable no-unused-vars */\n ...rest\n } = this.props\n\n if(hide && !keepContents)\n return \n\n let classesAr = []\n\n for (let device in DEVICES) {\n if (!Object.prototype.hasOwnProperty.call(DEVICES, device)) {\n continue\n }\n let deviceClass = DEVICES[device]\n if(device in this.props) {\n let val = this.props[device]\n\n if(val < 1) {\n classesAr.push(\"none\" + deviceClass)\n continue\n }\n\n classesAr.push(\"block\" + deviceClass)\n classesAr.push(\"col-\" + val + deviceClass)\n }\n }\n\n if (hide) {\n classesAr.push(\"hidden\")\n }\n\n let classes = xclass(rest.className, ...classesAr)\n\n return (\n
    \n )\n }\n\n}\n\nCol.propTypes = {\n hide: PropTypes.bool,\n keepContents: PropTypes.bool,\n mobile: PropTypes.number,\n tablet: PropTypes.number,\n desktop: PropTypes.number,\n large: PropTypes.number,\n className: PropTypes.string\n}\n\nexport class Row extends React.Component {\n\n render() {\n return
    \n }\n\n}\n\nRow.propTypes = {\n className: PropTypes.string\n}\n\nexport class Button extends React.Component {\n\n static propTypes = {\n className: PropTypes.string\n }\n\n static defaultProps = {\n className: \"\"\n }\n\n render() {\n return
    \n
    \n {curlBlock}\n
    \n \n )\n }\n\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\n\nexport default class Schemes extends React.Component {\n\n static propTypes = {\n specActions: PropTypes.object.isRequired,\n schemes: PropTypes.object.isRequired,\n currentScheme: PropTypes.string.isRequired,\n path: PropTypes.string,\n method: PropTypes.string,\n }\n\n UNSAFE_componentWillMount() {\n let { schemes } = this.props\n\n //fire 'change' event to set default 'value' of select\n this.setScheme(schemes.first())\n }\n\n UNSAFE_componentWillReceiveProps(nextProps) {\n if ( !this.props.currentScheme || !nextProps.schemes.includes(this.props.currentScheme) ) {\n // if we don't have a selected currentScheme or if our selected scheme is no longer an option,\n // then fire 'change' event and select the first scheme in the list of options\n this.setScheme(nextProps.schemes.first())\n }\n }\n\n onChange =( e ) => {\n this.setScheme( e.target.value )\n }\n\n setScheme = ( value ) => {\n let { path, method, specActions } = this.props\n\n specActions.setScheme( value, path, method )\n }\n\n render() {\n let { schemes, currentScheme } = this.props\n\n return (\n \n )\n }\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\n\nexport default class SchemesContainer extends React.Component {\n\n static propTypes = {\n specActions: PropTypes.object.isRequired,\n specSelectors: PropTypes.object.isRequired,\n getComponent: PropTypes.func.isRequired\n }\n\n render () {\n const {specActions, specSelectors, getComponent} = this.props\n\n const currentScheme = specSelectors.operationScheme()\n const schemes = specSelectors.schemes()\n\n const Schemes = getComponent(\"schemes\")\n\n const schemesArePresent = schemes && schemes.size\n\n return schemesArePresent ? (\n \n ) : null\n }\n}\n","import React, { Component } from \"react\"\nimport PropTypes from \"prop-types\"\nimport ImPropTypes from \"react-immutable-proptypes\"\nimport Im from \"immutable\"\n\nexport default class ModelCollapse extends Component {\n static propTypes = {\n collapsedContent: PropTypes.any,\n expanded: PropTypes.bool,\n children: PropTypes.any,\n title: PropTypes.element,\n modelName: PropTypes.string,\n classes: PropTypes.string,\n onToggle: PropTypes.func,\n hideSelfOnExpand: PropTypes.bool,\n layoutActions: PropTypes.object,\n layoutSelectors: PropTypes.object.isRequired,\n specPath: ImPropTypes.list.isRequired,\n }\n\n static defaultProps = {\n collapsedContent: \"{...}\",\n expanded: false,\n title: null,\n onToggle: () => {},\n hideSelfOnExpand: false,\n specPath: Im.List([]),\n }\n\n constructor(props, context) {\n super(props, context)\n\n let { expanded, collapsedContent } = this.props\n\n this.state = {\n expanded : expanded,\n collapsedContent: collapsedContent || ModelCollapse.defaultProps.collapsedContent\n }\n }\n\n componentDidMount() {\n const { hideSelfOnExpand, expanded, modelName } = this.props\n if(hideSelfOnExpand && expanded) {\n // We just mounted pre-expanded, and we won't be going back..\n // So let's give our parent an `onToggle` call..\n // Since otherwise it will never be called.\n this.props.onToggle(modelName, expanded)\n }\n }\n\n UNSAFE_componentWillReceiveProps(nextProps){\n if(this.props.expanded !== nextProps.expanded){\n this.setState({expanded: nextProps.expanded})\n }\n }\n\n toggleCollapsed=()=>{\n if(this.props.onToggle){\n this.props.onToggle(this.props.modelName,!this.state.expanded)\n }\n\n this.setState({\n expanded: !this.state.expanded\n })\n }\n\n onLoad = (ref) => {\n if (ref && this.props.layoutSelectors) {\n const scrollToKey = this.props.layoutSelectors.getScrollToKey()\n\n if( Im.is(scrollToKey, this.props.specPath) ) this.toggleCollapsed()\n this.props.layoutActions.readyToScroll(this.props.specPath, ref.parentElement)\n }\n }\n\n render () {\n const { title, classes } = this.props\n\n if(this.state.expanded ) {\n if(this.props.hideSelfOnExpand) {\n return \n {this.props.children}\n \n }\n }\n\n return (\n \n \n\n { this.state.expanded && this.props.children }\n \n )\n }\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\nimport ImPropTypes from \"react-immutable-proptypes\"\nimport cx from \"classnames\"\nimport randomBytes from \"randombytes\"\n\nexport default class ModelExample extends React.Component {\n static propTypes = {\n getComponent: PropTypes.func.isRequired,\n specSelectors: PropTypes.object.isRequired,\n schema: PropTypes.object.isRequired,\n example: PropTypes.any.isRequired,\n isExecute: PropTypes.bool,\n getConfigs: PropTypes.func.isRequired,\n specPath: ImPropTypes.list.isRequired,\n includeReadOnly: PropTypes.bool,\n includeWriteOnly: PropTypes.bool,\n }\n\n constructor(props, context) {\n super(props, context)\n let { getConfigs, isExecute } = this.props\n let { defaultModelRendering } = getConfigs()\n\n let activeTab = defaultModelRendering\n\n if (defaultModelRendering !== \"example\" && defaultModelRendering !== \"model\") {\n activeTab = \"example\"\n }\n\n if(isExecute) {\n activeTab = \"example\"\n }\n\n this.state = {\n activeTab,\n }\n }\n\n activeTab = ( e ) => {\n let { target : { dataset : { name } } } = e\n\n this.setState({\n activeTab: name\n })\n }\n\n UNSAFE_componentWillReceiveProps(nextProps) {\n if (\n nextProps.isExecute &&\n !this.props.isExecute &&\n this.props.example\n ) {\n this.setState({ activeTab: \"example\" })\n }\n }\n\n render() {\n let { getComponent, specSelectors, schema, example, isExecute, getConfigs, specPath, includeReadOnly, includeWriteOnly } = this.props\n let { defaultModelExpandDepth } = getConfigs()\n const ModelWrapper = getComponent(\"ModelWrapper\")\n const HighlightCode = getComponent(\"highlightCode\")\n const exampleTabId = randomBytes(5).toString(\"base64\")\n const examplePanelId = randomBytes(5).toString(\"base64\")\n const modelTabId = randomBytes(5).toString(\"base64\")\n const modelPanelId = randomBytes(5).toString(\"base64\")\n\n let isOAS3 = specSelectors.isOAS3()\n\n return (\n
    \n
      \n
    • \n \n {isExecute ? \"Edit Value\" : \"Example Value\"}\n \n
    • \n { schema && (\n
    • \n \n {isOAS3 ? \"Schema\" : \"Model\" }\n \n
    • \n )}\n
    \n {this.state.activeTab === \"example\" && (\n \n {example ? example : (\n \n )}\n
    \n )}\n\n {this.state.activeTab === \"model\" && (\n \n \n \n )}\n \n )\n }\n\n}\n","import React, { Component, } from \"react\"\nimport PropTypes from \"prop-types\"\nimport ImPropTypes from \"react-immutable-proptypes\"\n\nexport default class ModelWrapper extends Component {\n\n static propTypes = {\n schema: PropTypes.object.isRequired,\n name: PropTypes.string,\n displayName: PropTypes.string,\n fullPath: PropTypes.array.isRequired,\n specPath: ImPropTypes.list.isRequired,\n getComponent: PropTypes.func.isRequired,\n getConfigs: PropTypes.func.isRequired,\n specSelectors: PropTypes.object.isRequired,\n expandDepth: PropTypes.number,\n layoutActions: PropTypes.object,\n layoutSelectors: PropTypes.object.isRequired,\n includeReadOnly: PropTypes.bool,\n includeWriteOnly: PropTypes.bool,\n }\n\n onToggle = (name,isShown) => {\n // If this prop is present, we'll have deepLinking for it\n if(this.props.layoutActions) {\n this.props.layoutActions.show(this.props.fullPath, isShown)\n }\n }\n\n render(){\n let { getComponent, getConfigs } = this.props\n const Model = getComponent(\"Model\")\n\n let expanded\n if(this.props.layoutSelectors) {\n // If this is prop is present, we'll have deepLinking for it\n expanded = this.props.layoutSelectors.isShown(this.props.fullPath)\n }\n\n return
    \n \n
    \n }\n}\n","import React, { Component } from \"react\"\nimport Im, { Map } from \"immutable\"\nimport PropTypes from \"prop-types\"\n\nexport default class Models extends Component {\n static propTypes = {\n getComponent: PropTypes.func,\n specSelectors: PropTypes.object,\n specActions: PropTypes.object.isRequired,\n layoutSelectors: PropTypes.object,\n layoutActions: PropTypes.object,\n getConfigs: PropTypes.func.isRequired\n }\n\n getSchemaBasePath = () => {\n const isOAS3 = this.props.specSelectors.isOAS3()\n return isOAS3 ? [\"components\", \"schemas\"] : [\"definitions\"]\n }\n\n getCollapsedContent = () => {\n return \" \"\n }\n\n handleToggle = (name, isExpanded) => {\n const { layoutActions } = this.props\n layoutActions.show([...this.getSchemaBasePath(), name], isExpanded)\n if(isExpanded) {\n this.props.specActions.requestResolvedSubtree([...this.getSchemaBasePath(), name])\n }\n }\n\n onLoadModels = (ref) => {\n if (ref) {\n this.props.layoutActions.readyToScroll(this.getSchemaBasePath(), ref)\n }\n }\n\n onLoadModel = (ref) => {\n if (ref) {\n const name = ref.getAttribute(\"data-name\")\n this.props.layoutActions.readyToScroll([...this.getSchemaBasePath(), name], ref)\n }\n }\n\n render(){\n let { specSelectors, getComponent, layoutSelectors, layoutActions, getConfigs } = this.props\n let definitions = specSelectors.definitions()\n let { docExpansion, defaultModelsExpandDepth } = getConfigs()\n if (!definitions.size || defaultModelsExpandDepth < 0) return null\n\n const specPathBase = this.getSchemaBasePath()\n let showModels = layoutSelectors.isShown(specPathBase, defaultModelsExpandDepth > 0 && docExpansion !== \"none\")\n const isOAS3 = specSelectors.isOAS3()\n\n const ModelWrapper = getComponent(\"ModelWrapper\")\n const Collapse = getComponent(\"Collapse\")\n const ModelCollapse = getComponent(\"ModelCollapse\")\n const JumpToPath = getComponent(\"JumpToPath\", true)\n\n return
    \n

    \n layoutActions.show(specPathBase, !showModels)}\n >\n {isOAS3 ? \"Schemas\" : \"Models\"}\n \n \n \n \n

    \n \n {\n definitions.entrySeq().map(([name])=>{\n\n const fullPath = [...specPathBase, name]\n const specPath = Im.List(fullPath)\n\n const schemaValue = specSelectors.specResolvedSubtree(fullPath)\n const rawSchemaValue = specSelectors.specJson().getIn(fullPath)\n\n const schema = Map.isMap(schemaValue) ? schemaValue : Im.Map()\n const rawSchema = Map.isMap(rawSchemaValue) ? rawSchemaValue : Im.Map()\n\n const displayName = schema.get(\"title\") || rawSchema.get(\"title\") || name\n const isShown = layoutSelectors.isShown(fullPath, false)\n\n if( isShown && (schema.size === 0 && rawSchema.size > 0) ) {\n // Firing an action in a container render is not great,\n // but it works for now.\n this.props.specActions.requestResolvedSubtree(fullPath)\n }\n\n const content = \n\n const title = \n \n {displayName}\n \n \n\n return
    \n \n 0 && isShown }\n >{content}\n
    \n }).toArray()\n }\n
    \n
    \n }\n}\n","import React from \"react\"\nimport ImPropTypes from \"react-immutable-proptypes\"\n\nconst EnumModel = ({ value, getComponent }) => {\n let ModelCollapse = getComponent(\"ModelCollapse\")\n let collapsedContent = Array [ { value.count() } ]\n return \n Enum:
    \n \n [ { value.join(\", \") } ]\n \n
    \n}\nEnumModel.propTypes = {\n value: ImPropTypes.iterable,\n getComponent: ImPropTypes.func\n}\n\nexport default EnumModel","import React, { Component, } from \"react\"\nimport PropTypes from \"prop-types\"\nimport { List } from \"immutable\"\nimport ImPropTypes from \"react-immutable-proptypes\"\nimport { sanitizeUrl } from \"core/utils\"\n\nconst braceOpen = \"{\"\nconst braceClose = \"}\"\nconst propClass = \"property\"\n\nexport default class ObjectModel extends Component {\n static propTypes = {\n schema: PropTypes.object.isRequired,\n getComponent: PropTypes.func.isRequired,\n getConfigs: PropTypes.func.isRequired,\n expanded: PropTypes.bool,\n onToggle: PropTypes.func,\n specSelectors: PropTypes.object.isRequired,\n name: PropTypes.string,\n displayName: PropTypes.string,\n isRef: PropTypes.bool,\n expandDepth: PropTypes.number,\n depth: PropTypes.number,\n specPath: ImPropTypes.list.isRequired,\n includeReadOnly: PropTypes.bool,\n includeWriteOnly: PropTypes.bool,\n }\n\n render(){\n let { schema, name, displayName, isRef, getComponent, getConfigs, depth, onToggle, expanded, specPath, ...otherProps } = this.props\n let { specSelectors,expandDepth, includeReadOnly, includeWriteOnly} = otherProps\n const { isOAS3 } = specSelectors\n\n if(!schema) {\n return null\n }\n\n const { showExtensions } = getConfigs()\n\n let description = schema.get(\"description\")\n let properties = schema.get(\"properties\")\n let additionalProperties = schema.get(\"additionalProperties\")\n let title = schema.get(\"title\") || displayName || name\n let requiredProperties = schema.get(\"required\")\n let infoProperties = schema\n .filter( ( v, key) => [\"maxProperties\", \"minProperties\", \"nullable\", \"example\"].indexOf(key) !== -1 )\n let deprecated = schema.get(\"deprecated\")\n let externalDocsUrl = schema.getIn([\"externalDocs\", \"url\"])\n let externalDocsDescription = schema.getIn([\"externalDocs\", \"description\"])\n\n const JumpToPath = getComponent(\"JumpToPath\", true)\n const Markdown = getComponent(\"Markdown\", true)\n const Model = getComponent(\"Model\")\n const ModelCollapse = getComponent(\"ModelCollapse\")\n const Property = getComponent(\"Property\")\n const Link = getComponent(\"Link\")\n\n const JumpToPathSection = () => {\n return \n }\n const collapsedContent = (\n { braceOpen }...{ braceClose }\n {\n isRef ? : \"\"\n }\n )\n\n const anyOf = specSelectors.isOAS3() ? schema.get(\"anyOf\") : null\n const oneOf = specSelectors.isOAS3() ? schema.get(\"oneOf\") : null\n const not = specSelectors.isOAS3() ? schema.get(\"not\") : null\n\n const titleEl = title && \n { isRef && schema.get(\"$$ref\") && { schema.get(\"$$ref\") } }\n { title }\n \n\n return \n \n\n { braceOpen }\n {\n !isRef ? null : \n }\n \n {\n \n {\n !description ? null : \n \n \n \n }\n {\n externalDocsUrl &&\n \n \n \n \n }\n {\n !deprecated ? null :\n \n \n \n \n }\n {\n !(properties && properties.size) ? null : properties.entrySeq().filter(\n ([, value]) => {\n return (!value.get(\"readOnly\") || includeReadOnly) &&\n (!value.get(\"writeOnly\") || includeWriteOnly)\n }\n ).map(\n ([key, value]) => {\n let isDeprecated = isOAS3() && value.get(\"deprecated\")\n let isRequired = List.isList(requiredProperties) && requiredProperties.contains(key)\n\n let classNames = [\"property-row\"]\n\n if (isDeprecated) {\n classNames.push(\"deprecated\")\n }\n\n if (isRequired) {\n classNames.push(\"required\")\n }\n\n return (\n \n \n )\n }).toArray()\n }\n {\n // empty row before extensions...\n !showExtensions ? null : \n }\n {\n !showExtensions ? null :\n schema.entrySeq().map(\n ([key, value]) => {\n if(key.slice(0,2) !== \"x-\") {\n return\n }\n\n const normalizedValue = !value ? null : value.toJS ? value.toJS() : value\n\n return (\n \n \n )\n }).toArray()\n }\n {\n !additionalProperties || !additionalProperties.size ? null\n : \n \n \n \n }\n {\n !anyOf ? null\n : \n \n \n \n }\n {\n !oneOf ? null\n : \n \n \n \n }\n {\n !not ? null\n : \n \n \n \n }\n
    description:\n \n
    \n externalDocs:\n \n {externalDocsDescription || externalDocsUrl}\n
    \n deprecated:\n \n true\n
    \n { key }{ isRequired && * }\n \n \n
     
    \n { key }\n \n { JSON.stringify(normalizedValue) }\n
    { \"< * >:\" }\n \n
    { \"anyOf ->\" }\n {anyOf.map((schema, k) => {\n return
    \n })}\n
    { \"oneOf ->\" }\n {oneOf.map((schema, k) => {\n return
    \n })}\n
    { \"not ->\" }\n
    \n \n
    \n
    \n }\n
    \n { braceClose }\n \n {\n infoProperties.size ? infoProperties.entrySeq().map( ( [ key, v ] ) => ) : null\n }\n
    \n }\n}\n","import React, { Component } from \"react\"\nimport PropTypes from \"prop-types\"\nimport ImPropTypes from \"react-immutable-proptypes\"\nimport { sanitizeUrl } from \"core/utils\"\n\nconst propClass = \"property\"\n\nexport default class ArrayModel extends Component {\n static propTypes = {\n schema: PropTypes.object.isRequired,\n getComponent: PropTypes.func.isRequired,\n getConfigs: PropTypes.func.isRequired,\n specSelectors: PropTypes.object.isRequired,\n name: PropTypes.string,\n displayName: PropTypes.string,\n required: PropTypes.bool,\n expandDepth: PropTypes.number,\n specPath: ImPropTypes.list.isRequired,\n depth: PropTypes.number,\n includeReadOnly: PropTypes.bool,\n includeWriteOnly: PropTypes.bool,\n }\n\n render(){\n let { getComponent, getConfigs, schema, depth, expandDepth, name, displayName, specPath } = this.props\n let description = schema.get(\"description\")\n let items = schema.get(\"items\")\n let title = schema.get(\"title\") || displayName || name\n let properties = schema.filter( ( v, key) => [\"type\", \"items\", \"description\", \"$$ref\", \"externalDocs\"].indexOf(key) === -1 )\n let externalDocsUrl = schema.getIn([\"externalDocs\", \"url\"])\n let externalDocsDescription = schema.getIn([\"externalDocs\", \"description\"])\n\n\n const Markdown = getComponent(\"Markdown\", true)\n const ModelCollapse = getComponent(\"ModelCollapse\")\n const Model = getComponent(\"Model\")\n const Property = getComponent(\"Property\")\n const Link = getComponent(\"Link\")\n\n const titleEl = title &&\n \n { title }\n \n\n /*\n Note: we set `name={null}` in below because we don't want\n the name of the current Model passed (and displayed) as the name of the array element Model\n */\n\n return \n \n [\n {\n properties.size ? properties.entrySeq().map( ( [ key, v ] ) => ) : null\n }\n {\n !description ? (properties.size ?
    : null) :\n \n }\n { externalDocsUrl &&\n
    \n {externalDocsDescription || externalDocsUrl}\n
    \n }\n \n \n \n ]\n
    \n
    \n }\n}\n","import React, { Component } from \"react\"\nimport PropTypes from \"prop-types\"\nimport { getExtensions, sanitizeUrl } from \"core/utils\"\n\nconst propClass = \"property primitive\"\n\nexport default class Primitive extends Component {\n static propTypes = {\n schema: PropTypes.object.isRequired,\n getComponent: PropTypes.func.isRequired,\n getConfigs: PropTypes.func.isRequired,\n name: PropTypes.string,\n displayName: PropTypes.string,\n depth: PropTypes.number,\n expandDepth: PropTypes.number\n }\n\n render() {\n let { schema, getComponent, getConfigs, name, displayName, depth, expandDepth } = this.props\n\n const { showExtensions } = getConfigs()\n\n if (!schema || !schema.get) {\n // don't render if schema isn't correctly formed\n return
    \n }\n\n let type = schema.get(\"type\")\n let format = schema.get(\"format\")\n let xml = schema.get(\"xml\")\n let enumArray = schema.get(\"enum\")\n let title = schema.get(\"title\") || displayName || name\n let description = schema.get(\"description\")\n let extensions = getExtensions(schema)\n let properties = schema\n .filter((_, key) => [\"enum\", \"type\", \"format\", \"description\", \"$$ref\", \"externalDocs\"].indexOf(key) === -1)\n .filterNot((_, key) => extensions.has(key))\n let externalDocsUrl = schema.getIn([\"externalDocs\", \"url\"])\n let externalDocsDescription = schema.getIn([\"externalDocs\", \"description\"])\n\n const Markdown = getComponent(\"Markdown\", true)\n const EnumModel = getComponent(\"EnumModel\")\n const Property = getComponent(\"Property\")\n const ModelCollapse = getComponent(\"ModelCollapse\")\n const Link = getComponent(\"Link\")\n\n const titleEl = title &&\n \n {title}\n \n\n return \n \n \n {name && depth > 1 && {title}}\n {type}\n {format && (${format})}\n {\n properties.size ? properties.entrySeq().map(([key, v]) => ) : null\n }\n {\n showExtensions && extensions.size ? extensions.entrySeq().map(([key, v]) => ) : null\n }\n {\n !description ? null :\n \n }\n {\n externalDocsUrl &&\n
    \n {externalDocsDescription || externalDocsUrl}\n
    \n }\n {\n xml && xml.size ? (
    xml:\n {\n xml.entrySeq().map(([key, v]) =>
       {key}: {String(v)}
    ).toArray()\n }\n
    ) : null\n }\n {\n enumArray && \n }\n
    \n
    \n
    \n }\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\n\nexport const Property = ({ propKey, propVal, propClass }) => {\n return (\n \n
    { propKey }: { String(propVal) }
    \n )\n}\nProperty.propTypes = {\n propKey: PropTypes.string,\n propVal: PropTypes.any,\n propClass: PropTypes.string\n}\n\nexport default Property\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\n\nexport default class TryItOutButton extends React.Component {\n\n static propTypes = {\n onTryoutClick: PropTypes.func,\n onResetClick: PropTypes.func,\n onCancelClick: PropTypes.func,\n enabled: PropTypes.bool, // Try it out is enabled, ie: the user has access to the form\n hasUserEditedBody: PropTypes.bool, // Try it out is enabled, ie: the user has access to the form\n isOAS3: PropTypes.bool, // Try it out is enabled, ie: the user has access to the form\n }\n\n static defaultProps = {\n onTryoutClick: Function.prototype,\n onCancelClick: Function.prototype,\n onResetClick: Function.prototype,\n enabled: false,\n hasUserEditedBody: false,\n isOAS3: false,\n }\n\n render() {\n const { onTryoutClick, onCancelClick, onResetClick, enabled, hasUserEditedBody, isOAS3 } = this.props\n\n const showReset = isOAS3 && hasUserEditedBody\n return (\n
    \n {\n enabled ? \n : \n\n }\n {\n showReset && \n }\n
    \n )\n }\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\n\nexport default class VersionPragmaFilter extends React.PureComponent {\n static propTypes = {\n isSwagger2: PropTypes.bool.isRequired,\n isOAS3: PropTypes.bool.isRequired,\n bypass: PropTypes.bool,\n alsoShow: PropTypes.element,\n children: PropTypes.any,\n }\n\n static defaultProps = {\n alsoShow: null,\n children: null,\n bypass: false,\n }\n\n render() {\n const { bypass, isSwagger2, isOAS3, alsoShow } = this.props\n\n if(bypass) {\n return
    { this.props.children }
    \n }\n\n if(isSwagger2 && isOAS3) {\n return
    \n {alsoShow}\n
    \n
    \n

    Unable to render this definition

    \n

    swagger and openapi fields cannot be present in the same Swagger or OpenAPI definition. Please remove one of the fields.

    \n

    Supported version fields are swagger: {\"\\\"2.0\\\"\"} and those that match openapi: 3.0.n (for example, openapi: 3.0.0).

    \n
    \n
    \n
    \n }\n\n if(!isSwagger2 && !isOAS3) {\n return
    \n {alsoShow}\n
    \n
    \n

    Unable to render this definition

    \n

    The provided definition does not specify a valid version field.

    \n

    Please indicate a valid Swagger or OpenAPI version field. Supported version fields are swagger: {\"\\\"2.0\\\"\"} and those that match openapi: 3.0.n (for example, openapi: 3.0.0).

    \n
    \n
    \n
    \n }\n\n return
    { this.props.children }
    \n }\n}\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\n\nconst VersionStamp = ({ version }) => {\n return
     { version } 
    \n}\n\nVersionStamp.propTypes = {\n version: PropTypes.string.isRequired\n}\n\nexport default VersionStamp\n","import React from \"react\"\nimport PropTypes from \"prop-types\"\n\nexport const DeepLink = ({ enabled, path, text }) => {\n return (\n e.preventDefault() : null}\n href={enabled ? `#/${path}` : null}>\n {text}\n \n )\n}\nDeepLink.propTypes = {\n enabled: PropTypes.bool,\n isShown: PropTypes.bool,\n path: PropTypes.string,\n text: PropTypes.node\n}\n\nexport default DeepLink\n","import React from \"react\"\nconst SvgAssets = () =>\n
    \n \n \n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n \n \n\n \n \n
    \n\nexport default SvgAssets\n","/**\n * @prettier\n */\nimport React from \"react\"\nimport PropTypes from \"prop-types\"\n\nexport default class BaseLayout extends React.Component {\n static propTypes = {\n errSelectors: PropTypes.object.isRequired,\n errActions: PropTypes.object.isRequired,\n specSelectors: PropTypes.object.isRequired,\n oas3Selectors: PropTypes.object.isRequired,\n oas3Actions: PropTypes.object.isRequired,\n getComponent: PropTypes.func.isRequired,\n }\n\n render() {\n const { errSelectors, specSelectors, getComponent } = this.props\n\n const SvgAssets = getComponent(\"SvgAssets\")\n const InfoContainer = getComponent(\"InfoContainer\", true)\n const VersionPragmaFilter = getComponent(\"VersionPragmaFilter\")\n const Operations = getComponent(\"operations\", true)\n const Models = getComponent(\"Models\", true)\n const Webhooks = getComponent(\"Webhooks\", true)\n const Row = getComponent(\"Row\")\n const Col = getComponent(\"Col\")\n const Errors = getComponent(\"errors\", true)\n\n const ServersContainer = getComponent(\"ServersContainer\", true)\n const SchemesContainer = getComponent(\"SchemesContainer\", true)\n const AuthorizeBtnContainer = getComponent(\"AuthorizeBtnContainer\", true)\n const FilterContainer = getComponent(\"FilterContainer\", true)\n const isSwagger2 = specSelectors.isSwagger2()\n const isOAS3 = specSelectors.isOAS3()\n const isOAS31 = specSelectors.isOAS31()\n\n const isSpecEmpty = !specSelectors.specStr()\n\n const loadingStatus = specSelectors.loadingStatus()\n\n let loadingMessage = null\n\n if (loadingStatus === \"loading\") {\n loadingMessage = (\n
    \n
    \n
    \n
    \n
    \n )\n }\n\n if (loadingStatus === \"failed\") {\n loadingMessage = (\n
    \n
    \n

    Failed to load API definition.

    \n \n
    \n
    \n )\n }\n\n if (loadingStatus === \"failedConfig\") {\n const lastErr = errSelectors.lastError()\n const lastErrMsg = lastErr ? lastErr.get(\"message\") : \"\"\n loadingMessage = (\n
    \n
    \n

    Failed to load remote configuration.

    \n

    {lastErrMsg}

    \n
    \n
    \n )\n }\n\n if (!loadingMessage && isSpecEmpty) {\n loadingMessage =

    No API definition provided.

    \n }\n\n if (loadingMessage) {\n return (\n
    \n
    {loadingMessage}
    \n
    \n )\n }\n\n const servers = specSelectors.servers()\n const schemes = specSelectors.schemes()\n\n const hasServers = servers && servers.size\n const hasSchemes = schemes && schemes.size\n const hasSecurityDefinitions = !!specSelectors.securityDefinitions()\n\n return (\n
    \n \n }\n >\n \n \n \n \n \n \n\n {hasServers || hasSchemes || hasSecurityDefinitions ? (\n
    \n \n {hasServers ? : null}\n {hasSchemes ? : null}\n {hasSecurityDefinitions ? : null}\n \n
    \n ) : null}\n\n \n\n \n \n \n \n \n\n {isOAS31 && (\n \n \n \n \n \n )}\n\n \n \n \n \n \n \n
    \n )\n }\n}\n","var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }\nvar y = x => () => x\nconst __WEBPACK_NAMESPACE_OBJECT__ = x({ [\"default\"]: () => __WEBPACK_EXTERNAL_MODULE_react_debounce_input_7ed3e068__[\"default\"] });","import React, { PureComponent, Component } from \"react\"\nimport PropTypes from \"prop-types\"\nimport { List, fromJS } from \"immutable\"\nimport cx from \"classnames\"\nimport ImPropTypes from \"react-immutable-proptypes\"\nimport DebounceInput from \"react-debounce-input\"\nimport { stringify } from \"core/utils\"\n\nconst noop = ()=> {}\nconst JsonSchemaPropShape = {\n getComponent: PropTypes.func.isRequired,\n value: PropTypes.any,\n onChange: PropTypes.func,\n keyName: PropTypes.any,\n fn: PropTypes.object.isRequired,\n schema: PropTypes.object,\n errors: ImPropTypes.list,\n required: PropTypes.bool,\n dispatchInitialValue: PropTypes.bool,\n description: PropTypes.any,\n disabled: PropTypes.bool,\n}\n\nconst JsonSchemaDefaultProps = {\n value: \"\",\n onChange: noop,\n schema: {},\n keyName: \"\",\n required: false,\n errors: List()\n}\n\nexport class JsonSchemaForm extends Component {\n\n static propTypes = JsonSchemaPropShape\n static defaultProps = JsonSchemaDefaultProps\n\n componentDidMount() {\n const { dispatchInitialValue, value, onChange } = this.props\n if(dispatchInitialValue) {\n onChange(value)\n } else if(dispatchInitialValue === false) {\n onChange(\"\")\n }\n }\n\n render() {\n let { schema, errors, value, onChange, getComponent, fn, disabled } = this.props\n const format = schema && schema.get ? schema.get(\"format\") : null\n const type = schema && schema.get ? schema.get(\"type\") : null\n\n let getComponentSilently = (name) => getComponent(name, false, { failSilently: true })\n let Comp = type ? format ?\n getComponentSilently(`JsonSchema_${type}_${format}`) :\n getComponentSilently(`JsonSchema_${type}`) :\n getComponent(\"JsonSchema_string\")\n if (!Comp) {\n Comp = getComponent(\"JsonSchema_string\")\n }\n return \n }\n}\n\nexport class JsonSchema_string extends Component {\n static propTypes = JsonSchemaPropShape\n static defaultProps = JsonSchemaDefaultProps\n onChange = (e) => {\n const value = this.props.schema && this.props.schema.get(\"type\") === \"file\" ? e.target.files[0] : e.target.value\n this.props.onChange(value, this.props.keyName)\n }\n onEnumChange = (val) => this.props.onChange(val)\n render() {\n let { getComponent, value, schema, errors, required, description, disabled } = this.props\n const enumValue = schema && schema.get ? schema.get(\"enum\") : null\n const format = schema && schema.get ? schema.get(\"format\") : null\n const type = schema && schema.get ? schema.get(\"type\") : null\n const schemaIn = schema && schema.get ? schema.get(\"in\") : null\n if (!value) {\n value = \"\" // value should not be null; this fixes a Debounce error\n }\n errors = errors.toJS ? errors.toJS() : []\n\n if ( enumValue ) {\n const Select = getComponent(\"Select\")\n return (\n )\n }\n else {\n return (\n \n )\n }\n }\n}\n\nexport class JsonSchema_array extends PureComponent {\n\n static propTypes = JsonSchemaPropShape\n static defaultProps = JsonSchemaDefaultProps\n\n constructor(props, context) {\n super(props, context)\n this.state = { value: valueOrEmptyList(props.value), schema: props.schema}\n }\n\n UNSAFE_componentWillReceiveProps(props) {\n const value = valueOrEmptyList(props.value)\n if(value !== this.state.value)\n this.setState({ value })\n\n if(props.schema !== this.state.schema)\n this.setState({ schema: props.schema })\n }\n\n onChange = () => {\n this.props.onChange(this.state.value)\n }\n\n onItemChange = (itemVal, i) => {\n this.setState(({ value }) => ({\n value: value.set(i, itemVal)\n }), this.onChange)\n }\n\n removeItem = (i) => {\n this.setState(({ value }) => ({\n value: value.delete(i)\n }), this.onChange)\n }\n\n addItem = () => {\n const { fn } = this.props\n let newValue = valueOrEmptyList(this.state.value)\n this.setState(() => ({\n value: newValue.push(fn.getSampleSchema(this.state.schema.get(\"items\"), false, {\n includeWriteOnly: true\n }))\n }), this.onChange)\n }\n\n onEnumChange = (value) => {\n this.setState(() => ({\n value: value\n }), this.onChange)\n }\n\n render() {\n let { getComponent, required, schema, errors, fn, disabled } = this.props\n\n errors = errors.toJS ? errors.toJS() : Array.isArray(errors) ? errors : []\n const arrayErrors = errors.filter(e => typeof e === \"string\")\n const needsRemoveError = errors.filter(e => e.needRemove !== undefined)\n .map(e => e.error)\n const value = this.state.value // expect Im List\n const shouldRenderValue =\n value && value.count && value.count() > 0 ? true : false\n const schemaItemsEnum = schema.getIn([\"items\", \"enum\"])\n const schemaItemsType = schema.getIn([\"items\", \"type\"])\n const schemaItemsFormat = schema.getIn([\"items\", \"format\"])\n const schemaItemsSchema = schema.get(\"items\")\n let ArrayItemsComponent\n let isArrayItemText = false\n let isArrayItemFile = (schemaItemsType === \"file\" || (schemaItemsType === \"string\" && schemaItemsFormat === \"binary\")) ? true : false\n if (schemaItemsType && schemaItemsFormat) {\n ArrayItemsComponent = getComponent(`JsonSchema_${schemaItemsType}_${schemaItemsFormat}`)\n } else if (schemaItemsType === \"boolean\" || schemaItemsType === \"array\" || schemaItemsType === \"object\") {\n ArrayItemsComponent = getComponent(`JsonSchema_${schemaItemsType}`)\n }\n // if ArrayItemsComponent not assigned or does not exist,\n // use default schemaItemsType === \"string\" & JsonSchemaArrayItemText component\n if (!ArrayItemsComponent && !isArrayItemFile) {\n isArrayItemText = true\n }\n\n if ( schemaItemsEnum ) {\n const Select = getComponent(\"Select\")\n return ()\n }\n}\n\nexport class JsonSchema_boolean extends Component {\n static propTypes = JsonSchemaPropShape\n static defaultProps = JsonSchemaDefaultProps\n\n onEnumChange = (val) => this.props.onChange(val)\n render() {\n let { getComponent, value, errors, schema, required, disabled } = this.props\n errors = errors.toJS ? errors.toJS() : []\n let enumValue = schema && schema.get ? schema.get(\"enum\") : null\n let allowEmptyValue = !enumValue || !required\n let booleanValue = !enumValue && [\"true\", \"false\"]\n const Select = getComponent(\"Select\")\n\n return (",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
    ",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e},i=t=>{const i=e(t);return i&&document.querySelector(i)?i:null},n=t=>{const i=e(t);return i?document.querySelector(i):null},s=e=>{e.dispatchEvent(new Event(t))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(t):null,a=(t,e,i)=>{Object.keys(i).forEach((n=>{const s=i[n],r=e[n],a=r&&o(r)?"element":null==(l=r)?`${l}`:{}.toString.call(l).match(/\s([a-z]+)/i)[1].toLowerCase();var l;if(!new RegExp(s).test(a))throw new TypeError(`${t.toUpperCase()}: Option "${n}" provided type "${a}" but expected type "${s}".`)}))},l=t=>!(!o(t)||0===t.getClientRects().length)&&"visible"===getComputedStyle(t).getPropertyValue("visibility"),c=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),h=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?h(t.parentNode):null},d=()=>{},u=t=>{t.offsetHeight},f=()=>{const{jQuery:t}=window;return t&&!document.body.hasAttribute("data-bs-no-jquery")?t:null},p=[],m=()=>"rtl"===document.documentElement.dir,g=t=>{var e;e=()=>{const e=f();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(p.length||document.addEventListener("DOMContentLoaded",(()=>{p.forEach((t=>t()))})),p.push(e)):e()},_=t=>{"function"==typeof t&&t()},b=(e,i,n=!0)=>{if(!n)return void _(e);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(i)+5;let r=!1;const a=({target:n})=>{n===i&&(r=!0,i.removeEventListener(t,a),_(e))};i.addEventListener(t,a),setTimeout((()=>{r||s(i)}),o)},v=(t,e,i,n)=>{let s=t.indexOf(e);if(-1===s)return t[!i&&n?t.length-1:0];const o=t.length;return s+=i?1:-1,n&&(s=(s+o)%o),t[Math.max(0,Math.min(s,o-1))]},y=/[^.]*(?=\..*)\.|.*/,w=/\..*/,E=/::\d+$/,A={};let T=1;const O={mouseenter:"mouseover",mouseleave:"mouseout"},C=/^(mouseenter|mouseleave)/i,k=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function L(t,e){return e&&`${e}::${T++}`||t.uidEvent||T++}function x(t){const e=L(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function D(t,e,i=null){const n=Object.keys(t);for(let s=0,o=n.length;sfunction(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};n?n=t(n):i=t(i)}const[o,r,a]=S(e,i,n),l=x(t),c=l[a]||(l[a]={}),h=D(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=L(r,e.replace(y,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(let a=o.length;a--;)if(o[a]===r)return s.delegateTarget=r,n.oneOff&&j.off(t,s.type,e,i),i.apply(r,[s]);return null}}(t,i,n):function(t,e){return function i(n){return n.delegateTarget=t,i.oneOff&&j.off(t,n.type,e),e.apply(t,[n])}}(t,i);u.delegationSelector=o?i:null,u.originalHandler=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function I(t,e,i,n,s){const o=D(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function P(t){return t=t.replace(w,""),O[t]||t}const j={on(t,e,i,n){N(t,e,i,n,!1)},one(t,e,i,n){N(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=S(e,i,n),a=r!==e,l=x(t),c=e.startsWith(".");if(void 0!==o){if(!l||!l[r])return;return void I(t,l,r,o,s?i:null)}c&&Object.keys(l).forEach((i=>{!function(t,e,i,n){const s=e[i]||{};Object.keys(s).forEach((o=>{if(o.includes(n)){const n=s[o];I(t,e,i,n.originalHandler,n.delegationSelector)}}))}(t,l,i,e.slice(1))}));const h=l[r]||{};Object.keys(h).forEach((i=>{const n=i.replace(E,"");if(!a||e.includes(n)){const e=h[i];I(t,l,r,e.originalHandler,e.delegationSelector)}}))},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=f(),s=P(e),o=e!==s,r=k.has(s);let a,l=!0,c=!0,h=!1,d=null;return o&&n&&(a=n.Event(e,i),n(t).trigger(a),l=!a.isPropagationStopped(),c=!a.isImmediatePropagationStopped(),h=a.isDefaultPrevented()),r?(d=document.createEvent("HTMLEvents"),d.initEvent(s,l,!0)):d=new CustomEvent(e,{bubbles:l,cancelable:!0}),void 0!==i&&Object.keys(i).forEach((t=>{Object.defineProperty(d,t,{get:()=>i[t]})})),h&&d.preventDefault(),c&&t.dispatchEvent(d),d.defaultPrevented&&void 0!==a&&a.preventDefault(),d}},M=new Map,H={set(t,e,i){M.has(t)||M.set(t,new Map);const n=M.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>M.has(t)&&M.get(t).get(e)||null,remove(t,e){if(!M.has(t))return;const i=M.get(t);i.delete(e),0===i.size&&M.delete(t)}};class B{constructor(t){(t=r(t))&&(this._element=t,H.set(this._element,this.constructor.DATA_KEY,this))}dispose(){H.remove(this._element,this.constructor.DATA_KEY),j.off(this._element,this.constructor.EVENT_KEY),Object.getOwnPropertyNames(this).forEach((t=>{this[t]=null}))}_queueCallback(t,e,i=!0){b(t,e,i)}static getInstance(t){return H.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.1.3"}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}}const R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,s=t.NAME;j.on(document,i,`[data-bs-dismiss="${s}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),c(this))return;const o=n(this)||this.closest(`.${s}`);t.getOrCreateInstance(o)[e]()}))};class W extends B{static get NAME(){return"alert"}close(){if(j.trigger(this._element,"close.bs.alert").defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),j.trigger(this._element,"closed.bs.alert"),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=W.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(W,"close"),g(W);const $='[data-bs-toggle="button"]';class z extends B{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=z.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}function q(t){return"true"===t||"false"!==t&&(t===Number(t).toString()?Number(t):""===t||"null"===t?null:t)}function F(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}j.on(document,"click.bs.button.data-api",$,(t=>{t.preventDefault();const e=t.target.closest($);z.getOrCreateInstance(e).toggle()})),g(z);const U={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${F(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${F(e)}`)},getDataAttributes(t){if(!t)return{};const e={};return Object.keys(t.dataset).filter((t=>t.startsWith("bs"))).forEach((i=>{let n=i.replace(/^bs/,"");n=n.charAt(0).toLowerCase()+n.slice(1,n.length),e[n]=q(t.dataset[i])})),e},getDataAttribute:(t,e)=>q(t.getAttribute(`data-bs-${F(e)}`)),offset(t){const e=t.getBoundingClientRect();return{top:e.top+window.pageYOffset,left:e.left+window.pageXOffset}},position:t=>({top:t.offsetTop,left:t.offsetLeft})},V={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode;for(;n&&n.nodeType===Node.ELEMENT_NODE&&3!==n.nodeType;)n.matches(e)&&i.push(n),n=n.parentNode;return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(", ");return this.find(e,t).filter((t=>!c(t)&&l(t)))}},K="carousel",X={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0,touch:!0},Y={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean",touch:"boolean"},Q="next",G="prev",Z="left",J="right",tt={ArrowLeft:J,ArrowRight:Z},et="slid.bs.carousel",it="active",nt=".active.carousel-item";class st extends B{constructor(t,e){super(t),this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this.touchStartX=0,this.touchDeltaX=0,this._config=this._getConfig(e),this._indicatorsElement=V.findOne(".carousel-indicators",this._element),this._touchSupported="ontouchstart"in document.documentElement||navigator.maxTouchPoints>0,this._pointerEvent=Boolean(window.PointerEvent),this._addEventListeners()}static get Default(){return X}static get NAME(){return K}next(){this._slide(Q)}nextWhenVisible(){!document.hidden&&l(this._element)&&this.next()}prev(){this._slide(G)}pause(t){t||(this._isPaused=!0),V.findOne(".carousel-item-next, .carousel-item-prev",this._element)&&(s(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null}cycle(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config&&this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))}to(t){this._activeElement=V.findOne(nt,this._element);const e=this._getItemIndex(this._activeElement);if(t>this._items.length-1||t<0)return;if(this._isSliding)return void j.one(this._element,et,(()=>this.to(t)));if(e===t)return this.pause(),void this.cycle();const i=t>e?Q:G;this._slide(i,this._items[t])}_getConfig(t){return t={...X,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},a(K,t,Y),t}_handleSwipe(){const t=Math.abs(this.touchDeltaX);if(t<=40)return;const e=t/this.touchDeltaX;this.touchDeltaX=0,e&&this._slide(e>0?J:Z)}_addEventListeners(){this._config.keyboard&&j.on(this._element,"keydown.bs.carousel",(t=>this._keydown(t))),"hover"===this._config.pause&&(j.on(this._element,"mouseenter.bs.carousel",(t=>this.pause(t))),j.on(this._element,"mouseleave.bs.carousel",(t=>this.cycle(t)))),this._config.touch&&this._touchSupported&&this._addTouchEventListeners()}_addTouchEventListeners(){const t=t=>this._pointerEvent&&("pen"===t.pointerType||"touch"===t.pointerType),e=e=>{t(e)?this.touchStartX=e.clientX:this._pointerEvent||(this.touchStartX=e.touches[0].clientX)},i=t=>{this.touchDeltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this.touchStartX},n=e=>{t(e)&&(this.touchDeltaX=e.clientX-this.touchStartX),this._handleSwipe(),"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((t=>this.cycle(t)),500+this._config.interval))};V.find(".carousel-item img",this._element).forEach((t=>{j.on(t,"dragstart.bs.carousel",(t=>t.preventDefault()))})),this._pointerEvent?(j.on(this._element,"pointerdown.bs.carousel",(t=>e(t))),j.on(this._element,"pointerup.bs.carousel",(t=>n(t))),this._element.classList.add("pointer-event")):(j.on(this._element,"touchstart.bs.carousel",(t=>e(t))),j.on(this._element,"touchmove.bs.carousel",(t=>i(t))),j.on(this._element,"touchend.bs.carousel",(t=>n(t))))}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=tt[t.key];e&&(t.preventDefault(),this._slide(e))}_getItemIndex(t){return this._items=t&&t.parentNode?V.find(".carousel-item",t.parentNode):[],this._items.indexOf(t)}_getItemByOrder(t,e){const i=t===Q;return v(this._items,e,i,this._config.wrap)}_triggerSlideEvent(t,e){const i=this._getItemIndex(t),n=this._getItemIndex(V.findOne(nt,this._element));return j.trigger(this._element,"slide.bs.carousel",{relatedTarget:t,direction:e,from:n,to:i})}_setActiveIndicatorElement(t){if(this._indicatorsElement){const e=V.findOne(".active",this._indicatorsElement);e.classList.remove(it),e.removeAttribute("aria-current");const i=V.find("[data-bs-target]",this._indicatorsElement);for(let e=0;e{j.trigger(this._element,et,{relatedTarget:o,direction:d,from:s,to:r})};if(this._element.classList.contains("slide")){o.classList.add(h),u(o),n.classList.add(c),o.classList.add(c);const t=()=>{o.classList.remove(c,h),o.classList.add(it),n.classList.remove(it,h,c),this._isSliding=!1,setTimeout(f,0)};this._queueCallback(t,n,!0)}else n.classList.remove(it),o.classList.add(it),this._isSliding=!1,f();a&&this.cycle()}_directionToOrder(t){return[J,Z].includes(t)?m()?t===Z?G:Q:t===Z?Q:G:t}_orderToDirection(t){return[Q,G].includes(t)?m()?t===G?Z:J:t===G?J:Z:t}static carouselInterface(t,e){const i=st.getOrCreateInstance(t,e);let{_config:n}=i;"object"==typeof e&&(n={...n,...e});const s="string"==typeof e?e:n.slide;if("number"==typeof e)i.to(e);else if("string"==typeof s){if(void 0===i[s])throw new TypeError(`No method named "${s}"`);i[s]()}else n.interval&&n.ride&&(i.pause(),i.cycle())}static jQueryInterface(t){return this.each((function(){st.carouselInterface(this,t)}))}static dataApiClickHandler(t){const e=n(this);if(!e||!e.classList.contains("carousel"))return;const i={...U.getDataAttributes(e),...U.getDataAttributes(this)},s=this.getAttribute("data-bs-slide-to");s&&(i.interval=!1),st.carouselInterface(e,i),s&&st.getInstance(e).to(s),t.preventDefault()}}j.on(document,"click.bs.carousel.data-api","[data-bs-slide], [data-bs-slide-to]",st.dataApiClickHandler),j.on(window,"load.bs.carousel.data-api",(()=>{const t=V.find('[data-bs-ride="carousel"]');for(let e=0,i=t.length;et===this._element));null!==s&&o.length&&(this._selector=s,this._triggerArray.push(e))}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return rt}static get NAME(){return ot}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t,e=[];if(this._config.parent){const t=V.find(ut,this._config.parent);e=V.find(".collapse.show, .collapse.collapsing",this._config.parent).filter((e=>!t.includes(e)))}const i=V.findOne(this._selector);if(e.length){const n=e.find((t=>i!==t));if(t=n?pt.getInstance(n):null,t&&t._isTransitioning)return}if(j.trigger(this._element,"show.bs.collapse").defaultPrevented)return;e.forEach((e=>{i!==e&&pt.getOrCreateInstance(e,{toggle:!1}).hide(),t||H.set(e,"bs.collapse",null)}));const n=this._getDimension();this._element.classList.remove(ct),this._element.classList.add(ht),this._element.style[n]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const s=`scroll${n[0].toUpperCase()+n.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(ht),this._element.classList.add(ct,lt),this._element.style[n]="",j.trigger(this._element,"shown.bs.collapse")}),this._element,!0),this._element.style[n]=`${this._element[s]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(j.trigger(this._element,"hide.bs.collapse").defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,u(this._element),this._element.classList.add(ht),this._element.classList.remove(ct,lt);const e=this._triggerArray.length;for(let t=0;t{this._isTransitioning=!1,this._element.classList.remove(ht),this._element.classList.add(ct),j.trigger(this._element,"hidden.bs.collapse")}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(lt)}_getConfig(t){return(t={...rt,...U.getDataAttributes(this._element),...t}).toggle=Boolean(t.toggle),t.parent=r(t.parent),a(ot,t,at),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=V.find(ut,this._config.parent);V.find(ft,this._config.parent).filter((e=>!t.includes(e))).forEach((t=>{const e=n(t);e&&this._addAriaAndCollapsedClass([t],this._isShown(e))}))}_addAriaAndCollapsedClass(t,e){t.length&&t.forEach((t=>{e?t.classList.remove(dt):t.classList.add(dt),t.setAttribute("aria-expanded",e)}))}static jQueryInterface(t){return this.each((function(){const e={};"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1);const i=pt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}j.on(document,"click.bs.collapse.data-api",ft,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();const e=i(this);V.find(e).forEach((t=>{pt.getOrCreateInstance(t,{toggle:!1}).toggle()}))})),g(pt);var mt="top",gt="bottom",_t="right",bt="left",vt="auto",yt=[mt,gt,_t,bt],wt="start",Et="end",At="clippingParents",Tt="viewport",Ot="popper",Ct="reference",kt=yt.reduce((function(t,e){return t.concat([e+"-"+wt,e+"-"+Et])}),[]),Lt=[].concat(yt,[vt]).reduce((function(t,e){return t.concat([e,e+"-"+wt,e+"-"+Et])}),[]),xt="beforeRead",Dt="read",St="afterRead",Nt="beforeMain",It="main",Pt="afterMain",jt="beforeWrite",Mt="write",Ht="afterWrite",Bt=[xt,Dt,St,Nt,It,Pt,jt,Mt,Ht];function Rt(t){return t?(t.nodeName||"").toLowerCase():null}function Wt(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function $t(t){return t instanceof Wt(t).Element||t instanceof Element}function zt(t){return t instanceof Wt(t).HTMLElement||t instanceof HTMLElement}function qt(t){return"undefined"!=typeof ShadowRoot&&(t instanceof Wt(t).ShadowRoot||t instanceof ShadowRoot)}const Ft={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];zt(s)&&Rt(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});zt(n)&&Rt(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function Ut(t){return t.split("-")[0]}function Vt(t,e){var i=t.getBoundingClientRect();return{width:i.width/1,height:i.height/1,top:i.top/1,right:i.right/1,bottom:i.bottom/1,left:i.left/1,x:i.left/1,y:i.top/1}}function Kt(t){var e=Vt(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Xt(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&qt(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function Yt(t){return Wt(t).getComputedStyle(t)}function Qt(t){return["table","td","th"].indexOf(Rt(t))>=0}function Gt(t){return(($t(t)?t.ownerDocument:t.document)||window.document).documentElement}function Zt(t){return"html"===Rt(t)?t:t.assignedSlot||t.parentNode||(qt(t)?t.host:null)||Gt(t)}function Jt(t){return zt(t)&&"fixed"!==Yt(t).position?t.offsetParent:null}function te(t){for(var e=Wt(t),i=Jt(t);i&&Qt(i)&&"static"===Yt(i).position;)i=Jt(i);return i&&("html"===Rt(i)||"body"===Rt(i)&&"static"===Yt(i).position)?e:i||function(t){var e=-1!==navigator.userAgent.toLowerCase().indexOf("firefox");if(-1!==navigator.userAgent.indexOf("Trident")&&zt(t)&&"fixed"===Yt(t).position)return null;for(var i=Zt(t);zt(i)&&["html","body"].indexOf(Rt(i))<0;){var n=Yt(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function ee(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}var ie=Math.max,ne=Math.min,se=Math.round;function oe(t,e,i){return ie(t,ne(e,i))}function re(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function ae(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const le={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=Ut(i.placement),l=ee(a),c=[bt,_t].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return re("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:ae(t,yt))}(s.padding,i),d=Kt(o),u="y"===l?mt:bt,f="y"===l?gt:_t,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=te(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,E=oe(v,w,y),A=l;i.modifiersData[n]=((e={})[A]=E,e.centerOffset=E-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Xt(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function ce(t){return t.split("-")[1]}var he={top:"auto",right:"auto",bottom:"auto",left:"auto"};function de(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=!0===h?function(t){var e=t.x,i=t.y,n=window.devicePixelRatio||1;return{x:se(se(e*n)/n)||0,y:se(se(i*n)/n)||0}}(r):"function"==typeof h?h(r):r,u=d.x,f=void 0===u?0:u,p=d.y,m=void 0===p?0:p,g=r.hasOwnProperty("x"),_=r.hasOwnProperty("y"),b=bt,v=mt,y=window;if(c){var w=te(i),E="clientHeight",A="clientWidth";w===Wt(i)&&"static"!==Yt(w=Gt(i)).position&&"absolute"===a&&(E="scrollHeight",A="scrollWidth"),w=w,s!==mt&&(s!==bt&&s!==_t||o!==Et)||(v=gt,m-=w[E]-n.height,m*=l?1:-1),s!==bt&&(s!==mt&&s!==gt||o!==Et)||(b=_t,f-=w[A]-n.width,f*=l?1:-1)}var T,O=Object.assign({position:a},c&&he);return l?Object.assign({},O,((T={})[v]=_?"0":"",T[b]=g?"0":"",T.transform=(y.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",T)):Object.assign({},O,((e={})[v]=_?m+"px":"",e[b]=g?f+"px":"",e.transform="",e))}const ue={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:Ut(e.placement),variation:ce(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,de(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,de(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var fe={passive:!0};const pe={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=Wt(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,fe)})),a&&l.addEventListener("resize",i.update,fe),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,fe)})),a&&l.removeEventListener("resize",i.update,fe)}},data:{}};var me={left:"right",right:"left",bottom:"top",top:"bottom"};function ge(t){return t.replace(/left|right|bottom|top/g,(function(t){return me[t]}))}var _e={start:"end",end:"start"};function be(t){return t.replace(/start|end/g,(function(t){return _e[t]}))}function ve(t){var e=Wt(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function ye(t){return Vt(Gt(t)).left+ve(t).scrollLeft}function we(t){var e=Yt(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ee(t){return["html","body","#document"].indexOf(Rt(t))>=0?t.ownerDocument.body:zt(t)&&we(t)?t:Ee(Zt(t))}function Ae(t,e){var i;void 0===e&&(e=[]);var n=Ee(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=Wt(n),r=s?[o].concat(o.visualViewport||[],we(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Ae(Zt(r)))}function Te(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function Oe(t,e){return e===Tt?Te(function(t){var e=Wt(t),i=Gt(t),n=e.visualViewport,s=i.clientWidth,o=i.clientHeight,r=0,a=0;return n&&(s=n.width,o=n.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(r=n.offsetLeft,a=n.offsetTop)),{width:s,height:o,x:r+ye(t),y:a}}(t)):zt(e)?function(t){var e=Vt(t);return e.top=e.top+t.clientTop,e.left=e.left+t.clientLeft,e.bottom=e.top+t.clientHeight,e.right=e.left+t.clientWidth,e.width=t.clientWidth,e.height=t.clientHeight,e.x=e.left,e.y=e.top,e}(e):Te(function(t){var e,i=Gt(t),n=ve(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=ie(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=ie(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+ye(t),l=-n.scrollTop;return"rtl"===Yt(s||i).direction&&(a+=ie(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Gt(t)))}function Ce(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?Ut(s):null,r=s?ce(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case mt:e={x:a,y:i.y-n.height};break;case gt:e={x:a,y:i.y+i.height};break;case _t:e={x:i.x+i.width,y:l};break;case bt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?ee(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case wt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Et:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ke(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.boundary,r=void 0===o?At:o,a=i.rootBoundary,l=void 0===a?Tt:a,c=i.elementContext,h=void 0===c?Ot:c,d=i.altBoundary,u=void 0!==d&&d,f=i.padding,p=void 0===f?0:f,m=re("number"!=typeof p?p:ae(p,yt)),g=h===Ot?Ct:Ot,_=t.rects.popper,b=t.elements[u?g:h],v=function(t,e,i){var n="clippingParents"===e?function(t){var e=Ae(Zt(t)),i=["absolute","fixed"].indexOf(Yt(t).position)>=0&&zt(t)?te(t):t;return $t(i)?e.filter((function(t){return $t(t)&&Xt(t,i)&&"body"!==Rt(t)})):[]}(t):[].concat(e),s=[].concat(n,[i]),o=s[0],r=s.reduce((function(e,i){var n=Oe(t,i);return e.top=ie(n.top,e.top),e.right=ne(n.right,e.right),e.bottom=ne(n.bottom,e.bottom),e.left=ie(n.left,e.left),e}),Oe(t,o));return r.width=r.right-r.left,r.height=r.bottom-r.top,r.x=r.left,r.y=r.top,r}($t(b)?b:b.contextElement||Gt(t.elements.popper),r,l),y=Vt(t.elements.reference),w=Ce({reference:y,element:_,strategy:"absolute",placement:s}),E=Te(Object.assign({},_,w)),A=h===Ot?E:y,T={top:v.top-A.top+m.top,bottom:A.bottom-v.bottom+m.bottom,left:v.left-A.left+m.left,right:A.right-v.right+m.right},O=t.modifiersData.offset;if(h===Ot&&O){var C=O[s];Object.keys(T).forEach((function(t){var e=[_t,gt].indexOf(t)>=0?1:-1,i=[mt,gt].indexOf(t)>=0?"y":"x";T[t]+=C[i]*e}))}return T}function Le(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?Lt:l,h=ce(n),d=h?a?kt:kt.filter((function(t){return ce(t)===h})):yt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ke(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[Ut(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const xe={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=Ut(g),b=l||(_!==g&&p?function(t){if(Ut(t)===vt)return[];var e=ge(t);return[be(t),e,be(e)]}(g):[ge(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(Ut(i)===vt?Le(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,E=new Map,A=!0,T=v[0],O=0;O=0,D=x?"width":"height",S=ke(e,{placement:C,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),N=x?L?_t:bt:L?gt:mt;y[D]>w[D]&&(N=ge(N));var I=ge(N),P=[];if(o&&P.push(S[k]<=0),a&&P.push(S[N]<=0,S[I]<=0),P.every((function(t){return t}))){T=C,A=!1;break}E.set(C,P)}if(A)for(var j=function(t){var e=v.find((function(e){var i=E.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},M=p?3:1;M>0&&"break"!==j(M);M--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function De(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function Se(t){return[mt,_t,gt,bt].some((function(e){return t[e]>=0}))}const Ne={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ke(e,{elementContext:"reference"}),a=ke(e,{altBoundary:!0}),l=De(r,n),c=De(a,s,o),h=Se(l),d=Se(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},Ie={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=Lt.reduce((function(t,i){return t[i]=function(t,e,i){var n=Ut(t),s=[bt,mt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[bt,_t].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},Pe={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=Ce({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},je={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ke(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=Ut(e.placement),b=ce(e.placement),v=!b,y=ee(_),w="x"===y?"y":"x",E=e.modifiersData.popperOffsets,A=e.rects.reference,T=e.rects.popper,O="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,C={x:0,y:0};if(E){if(o||a){var k="y"===y?mt:bt,L="y"===y?gt:_t,x="y"===y?"height":"width",D=E[y],S=E[y]+g[k],N=E[y]-g[L],I=f?-T[x]/2:0,P=b===wt?A[x]:T[x],j=b===wt?-T[x]:-A[x],M=e.elements.arrow,H=f&&M?Kt(M):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},R=B[k],W=B[L],$=oe(0,A[x],H[x]),z=v?A[x]/2-I-$-R-O:P-$-R-O,q=v?-A[x]/2+I+$+W+O:j+$+W+O,F=e.elements.arrow&&te(e.elements.arrow),U=F?"y"===y?F.clientTop||0:F.clientLeft||0:0,V=e.modifiersData.offset?e.modifiersData.offset[e.placement][y]:0,K=E[y]+z-V-U,X=E[y]+q-V;if(o){var Y=oe(f?ne(S,K):S,D,f?ie(N,X):N);E[y]=Y,C[y]=Y-D}if(a){var Q="x"===y?mt:bt,G="x"===y?gt:_t,Z=E[w],J=Z+g[Q],tt=Z-g[G],et=oe(f?ne(J,K):J,Z,f?ie(tt,X):tt);E[w]=et,C[w]=et-Z}}e.modifiersData[n]=C}},requiresIfExists:["offset"]};function Me(t,e,i){void 0===i&&(i=!1);var n=zt(e);zt(e)&&function(t){var e=t.getBoundingClientRect();e.width,t.offsetWidth,e.height,t.offsetHeight}(e);var s,o,r=Gt(e),a=Vt(t),l={scrollLeft:0,scrollTop:0},c={x:0,y:0};return(n||!n&&!i)&&(("body"!==Rt(e)||we(r))&&(l=(s=e)!==Wt(s)&&zt(s)?{scrollLeft:(o=s).scrollLeft,scrollTop:o.scrollTop}:ve(s)),zt(e)?((c=Vt(e)).x+=e.clientLeft,c.y+=e.clientTop):r&&(c.x=ye(r))),{x:a.left+l.scrollLeft-c.x,y:a.top+l.scrollTop-c.y,width:a.width,height:a.height}}function He(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var Be={placement:"bottom",modifiers:[],strategy:"absolute"};function Re(){for(var t=arguments.length,e=new Array(t),i=0;ij.on(t,"mouseover",d))),this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(Je),this._element.classList.add(Je),j.trigger(this._element,"shown.bs.dropdown",t)}hide(){if(c(this._element)||!this._isShown(this._menu))return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){j.trigger(this._element,"hide.bs.dropdown",t).defaultPrevented||("ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>j.off(t,"mouseover",d))),this._popper&&this._popper.destroy(),this._menu.classList.remove(Je),this._element.classList.remove(Je),this._element.setAttribute("aria-expanded","false"),U.removeDataAttribute(this._menu,"popper"),j.trigger(this._element,"hidden.bs.dropdown",t))}_getConfig(t){if(t={...this.constructor.Default,...U.getDataAttributes(this._element),...t},a(Ue,t,this.constructor.DefaultType),"object"==typeof t.reference&&!o(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${Ue.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(t){if(void 0===Fe)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org)");let e=this._element;"parent"===this._config.reference?e=t:o(this._config.reference)?e=r(this._config.reference):"object"==typeof this._config.reference&&(e=this._config.reference);const i=this._getPopperConfig(),n=i.modifiers.find((t=>"applyStyles"===t.name&&!1===t.enabled));this._popper=qe(e,this._menu,i),n&&U.setDataAttribute(this._menu,"popper","static")}_isShown(t=this._element){return t.classList.contains(Je)}_getMenuElement(){return V.next(this._element,ei)[0]}_getPlacement(){const t=this._element.parentNode;if(t.classList.contains("dropend"))return ri;if(t.classList.contains("dropstart"))return ai;const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?ni:ii:e?oi:si}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return"static"===this._config.display&&(t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..."function"==typeof this._config.popperConfig?this._config.popperConfig(t):this._config.popperConfig}}_selectMenuItem({key:t,target:e}){const i=V.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter(l);i.length&&v(i,e,t===Ye,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=hi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(t&&(2===t.button||"keyup"===t.type&&"Tab"!==t.key))return;const e=V.find(ti);for(let i=0,n=e.length;ie+t)),this._setElementAttributes(di,"paddingRight",(e=>e+t)),this._setElementAttributes(ui,"marginRight",(e=>e-t))}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t)[e];t.style[e]=`${i(Number.parseFloat(s))}px`}))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,"paddingRight"),this._resetElementAttributes(di,"paddingRight"),this._resetElementAttributes(ui,"marginRight")}_saveInitialAttribute(t,e){const i=t.style[e];i&&U.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=U.getDataAttribute(t,e);void 0===i?t.style.removeProperty(e):(U.removeDataAttribute(t,e),t.style[e]=i)}))}_applyManipulationCallback(t,e){o(t)?e(t):V.find(t,this._element).forEach(e)}isOverflowing(){return this.getWidth()>0}}const pi={className:"modal-backdrop",isVisible:!0,isAnimated:!1,rootElement:"body",clickCallback:null},mi={className:"string",isVisible:"boolean",isAnimated:"boolean",rootElement:"(element|string)",clickCallback:"(function|null)"},gi="show",_i="mousedown.bs.backdrop";class bi{constructor(t){this._config=this._getConfig(t),this._isAppended=!1,this._element=null}show(t){this._config.isVisible?(this._append(),this._config.isAnimated&&u(this._getElement()),this._getElement().classList.add(gi),this._emulateAnimation((()=>{_(t)}))):_(t)}hide(t){this._config.isVisible?(this._getElement().classList.remove(gi),this._emulateAnimation((()=>{this.dispose(),_(t)}))):_(t)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_getConfig(t){return(t={...pi,..."object"==typeof t?t:{}}).rootElement=r(t.rootElement),a("backdrop",t,mi),t}_append(){this._isAppended||(this._config.rootElement.append(this._getElement()),j.on(this._getElement(),_i,(()=>{_(this._config.clickCallback)})),this._isAppended=!0)}dispose(){this._isAppended&&(j.off(this._element,_i),this._element.remove(),this._isAppended=!1)}_emulateAnimation(t){b(t,this._getElement(),this._config.isAnimated)}}const vi={trapElement:null,autofocus:!0},yi={trapElement:"element",autofocus:"boolean"},wi=".bs.focustrap",Ei="backward";class Ai{constructor(t){this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}activate(){const{trapElement:t,autofocus:e}=this._config;this._isActive||(e&&t.focus(),j.off(document,wi),j.on(document,"focusin.bs.focustrap",(t=>this._handleFocusin(t))),j.on(document,"keydown.tab.bs.focustrap",(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,j.off(document,wi))}_handleFocusin(t){const{target:e}=t,{trapElement:i}=this._config;if(e===document||e===i||i.contains(e))return;const n=V.focusableChildren(i);0===n.length?i.focus():this._lastTabNavDirection===Ei?n[n.length-1].focus():n[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?Ei:"forward")}_getConfig(t){return t={...vi,..."object"==typeof t?t:{}},a("focustrap",t,yi),t}}const Ti="modal",Oi="Escape",Ci={backdrop:!0,keyboard:!0,focus:!0},ki={backdrop:"(boolean|string)",keyboard:"boolean",focus:"boolean"},Li="hidden.bs.modal",xi="show.bs.modal",Di="resize.bs.modal",Si="click.dismiss.bs.modal",Ni="keydown.dismiss.bs.modal",Ii="mousedown.dismiss.bs.modal",Pi="modal-open",ji="show",Mi="modal-static";class Hi extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._dialog=V.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._ignoreBackdropClick=!1,this._isTransitioning=!1,this._scrollBar=new fi}static get Default(){return Ci}static get NAME(){return Ti}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||j.trigger(this._element,xi,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isAnimated()&&(this._isTransitioning=!0),this._scrollBar.hide(),document.body.classList.add(Pi),this._adjustDialog(),this._setEscapeEvent(),this._setResizeEvent(),j.on(this._dialog,Ii,(()=>{j.one(this._element,"mouseup.dismiss.bs.modal",(t=>{t.target===this._element&&(this._ignoreBackdropClick=!0)}))})),this._showBackdrop((()=>this._showElement(t))))}hide(){if(!this._isShown||this._isTransitioning)return;if(j.trigger(this._element,"hide.bs.modal").defaultPrevented)return;this._isShown=!1;const t=this._isAnimated();t&&(this._isTransitioning=!0),this._setEscapeEvent(),this._setResizeEvent(),this._focustrap.deactivate(),this._element.classList.remove(ji),j.off(this._element,Si),j.off(this._dialog,Ii),this._queueCallback((()=>this._hideModal()),this._element,t)}dispose(){[window,this._dialog].forEach((t=>j.off(t,".bs.modal"))),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new bi({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Ai({trapElement:this._element})}_getConfig(t){return t={...Ci,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},a(Ti,t,ki),t}_showElement(t){const e=this._isAnimated(),i=V.findOne(".modal-body",this._dialog);this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0,i&&(i.scrollTop=0),e&&u(this._element),this._element.classList.add(ji),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,j.trigger(this._element,"shown.bs.modal",{relatedTarget:t})}),this._dialog,e)}_setEscapeEvent(){this._isShown?j.on(this._element,Ni,(t=>{this._config.keyboard&&t.key===Oi?(t.preventDefault(),this.hide()):this._config.keyboard||t.key!==Oi||this._triggerBackdropTransition()})):j.off(this._element,Ni)}_setResizeEvent(){this._isShown?j.on(window,Di,(()=>this._adjustDialog())):j.off(window,Di)}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(Pi),this._resetAdjustments(),this._scrollBar.reset(),j.trigger(this._element,Li)}))}_showBackdrop(t){j.on(this._element,Si,(t=>{this._ignoreBackdropClick?this._ignoreBackdropClick=!1:t.target===t.currentTarget&&(!0===this._config.backdrop?this.hide():"static"===this._config.backdrop&&this._triggerBackdropTransition())})),this._backdrop.show(t)}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(j.trigger(this._element,"hidePrevented.bs.modal").defaultPrevented)return;const{classList:t,scrollHeight:e,style:i}=this._element,n=e>document.documentElement.clientHeight;!n&&"hidden"===i.overflowY||t.contains(Mi)||(n||(i.overflowY="hidden"),t.add(Mi),this._queueCallback((()=>{t.remove(Mi),n||this._queueCallback((()=>{i.overflowY=""}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;(!i&&t&&!m()||i&&!t&&m())&&(this._element.style.paddingLeft=`${e}px`),(i&&!t&&!m()||!i&&t&&m())&&(this._element.style.paddingRight=`${e}px`)}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=Hi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}j.on(document,"click.bs.modal.data-api",'[data-bs-toggle="modal"]',(function(t){const e=n(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),j.one(e,xi,(t=>{t.defaultPrevented||j.one(e,Li,(()=>{l(this)&&this.focus()}))}));const i=V.findOne(".modal.show");i&&Hi.getInstance(i).hide(),Hi.getOrCreateInstance(e).toggle(this)})),R(Hi),g(Hi);const Bi="offcanvas",Ri={backdrop:!0,keyboard:!0,scroll:!1},Wi={backdrop:"boolean",keyboard:"boolean",scroll:"boolean"},$i="show",zi=".offcanvas.show",qi="hidden.bs.offcanvas";class Fi extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get NAME(){return Bi}static get Default(){return Ri}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||j.trigger(this._element,"show.bs.offcanvas",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._element.style.visibility="visible",this._backdrop.show(),this._config.scroll||(new fi).hide(),this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add($i),this._queueCallback((()=>{this._config.scroll||this._focustrap.activate(),j.trigger(this._element,"shown.bs.offcanvas",{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(j.trigger(this._element,"hide.bs.offcanvas").defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.remove($i),this._backdrop.hide(),this._queueCallback((()=>{this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._element.style.visibility="hidden",this._config.scroll||(new fi).reset(),j.trigger(this._element,qi)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_getConfig(t){return t={...Ri,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},a(Bi,t,Wi),t}_initializeBackDrop(){return new bi({className:"offcanvas-backdrop",isVisible:this._config.backdrop,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:()=>this.hide()})}_initializeFocusTrap(){return new Ai({trapElement:this._element})}_addEventListeners(){j.on(this._element,"keydown.dismiss.bs.offcanvas",(t=>{this._config.keyboard&&"Escape"===t.key&&this.hide()}))}static jQueryInterface(t){return this.each((function(){const e=Fi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}j.on(document,"click.bs.offcanvas.data-api",'[data-bs-toggle="offcanvas"]',(function(t){const e=n(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),c(this))return;j.one(e,qi,(()=>{l(this)&&this.focus()}));const i=V.findOne(zi);i&&i!==e&&Fi.getInstance(i).hide(),Fi.getOrCreateInstance(e).toggle(this)})),j.on(window,"load.bs.offcanvas.data-api",(()=>V.find(zi).forEach((t=>Fi.getOrCreateInstance(t).show())))),R(Fi),g(Fi);const Ui=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Vi=/^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i,Ki=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,Xi=(t,e)=>{const i=t.nodeName.toLowerCase();if(e.includes(i))return!Ui.has(i)||Boolean(Vi.test(t.nodeValue)||Ki.test(t.nodeValue));const n=e.filter((t=>t instanceof RegExp));for(let t=0,e=n.length;t{Xi(t,r)||i.removeAttribute(t.nodeName)}))}return n.body.innerHTML}const Qi="tooltip",Gi=new Set(["sanitize","allowList","sanitizeFn"]),Zi={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(array|string|function)",container:"(string|element|boolean)",fallbackPlacements:"array",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",allowList:"object",popperConfig:"(null|object|function)"},Ji={AUTO:"auto",TOP:"top",RIGHT:m()?"left":"right",BOTTOM:"bottom",LEFT:m()?"right":"left"},tn={animation:!0,template:'',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:[0,0],container:!1,fallbackPlacements:["top","right","bottom","left"],boundary:"clippingParents",customClass:"",sanitize:!0,sanitizeFn:null,allowList:{"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},popperConfig:null},en={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"},nn="fade",sn="show",on="show",rn="out",an=".tooltip-inner",ln=".modal",cn="hide.bs.modal",hn="hover",dn="focus";class un extends B{constructor(t,e){if(void 0===Fe)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t),this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this._config=this._getConfig(e),this.tip=null,this._setListeners()}static get Default(){return tn}static get NAME(){return Qi}static get Event(){return en}static get DefaultType(){return Zi}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(t){if(this._isEnabled)if(t){const e=this._initializeOnDelegatedTarget(t);e._activeTrigger.click=!e._activeTrigger.click,e._isWithActiveTrigger()?e._enter(null,e):e._leave(null,e)}else{if(this.getTipElement().classList.contains(sn))return void this._leave(null,this);this._enter(null,this)}}dispose(){clearTimeout(this._timeout),j.off(this._element.closest(ln),cn,this._hideModalHandler),this.tip&&this.tip.remove(),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this.isWithContent()||!this._isEnabled)return;const t=j.trigger(this._element,this.constructor.Event.SHOW),e=h(this._element),i=null===e?this._element.ownerDocument.documentElement.contains(this._element):e.contains(this._element);if(t.defaultPrevented||!i)return;"tooltip"===this.constructor.NAME&&this.tip&&this.getTitle()!==this.tip.querySelector(an).innerHTML&&(this._disposePopper(),this.tip.remove(),this.tip=null);const n=this.getTipElement(),s=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME);n.setAttribute("id",s),this._element.setAttribute("aria-describedby",s),this._config.animation&&n.classList.add(nn);const o="function"==typeof this._config.placement?this._config.placement.call(this,n,this._element):this._config.placement,r=this._getAttachment(o);this._addAttachmentClass(r);const{container:a}=this._config;H.set(n,this.constructor.DATA_KEY,this),this._element.ownerDocument.documentElement.contains(this.tip)||(a.append(n),j.trigger(this._element,this.constructor.Event.INSERTED)),this._popper?this._popper.update():this._popper=qe(this._element,n,this._getPopperConfig(r)),n.classList.add(sn);const l=this._resolvePossibleFunction(this._config.customClass);l&&n.classList.add(...l.split(" ")),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>{j.on(t,"mouseover",d)}));const c=this.tip.classList.contains(nn);this._queueCallback((()=>{const t=this._hoverState;this._hoverState=null,j.trigger(this._element,this.constructor.Event.SHOWN),t===rn&&this._leave(null,this)}),this.tip,c)}hide(){if(!this._popper)return;const t=this.getTipElement();if(j.trigger(this._element,this.constructor.Event.HIDE).defaultPrevented)return;t.classList.remove(sn),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>j.off(t,"mouseover",d))),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1;const e=this.tip.classList.contains(nn);this._queueCallback((()=>{this._isWithActiveTrigger()||(this._hoverState!==on&&t.remove(),this._cleanTipClass(),this._element.removeAttribute("aria-describedby"),j.trigger(this._element,this.constructor.Event.HIDDEN),this._disposePopper())}),this.tip,e),this._hoverState=""}update(){null!==this._popper&&this._popper.update()}isWithContent(){return Boolean(this.getTitle())}getTipElement(){if(this.tip)return this.tip;const t=document.createElement("div");t.innerHTML=this._config.template;const e=t.children[0];return this.setContent(e),e.classList.remove(nn,sn),this.tip=e,this.tip}setContent(t){this._sanitizeAndSetContent(t,this.getTitle(),an)}_sanitizeAndSetContent(t,e,i){const n=V.findOne(i,t);e||!n?this.setElementContent(n,e):n.remove()}setElementContent(t,e){if(null!==t)return o(e)?(e=r(e),void(this._config.html?e.parentNode!==t&&(t.innerHTML="",t.append(e)):t.textContent=e.textContent)):void(this._config.html?(this._config.sanitize&&(e=Yi(e,this._config.allowList,this._config.sanitizeFn)),t.innerHTML=e):t.textContent=e)}getTitle(){const t=this._element.getAttribute("data-bs-original-title")||this._config.title;return this._resolvePossibleFunction(t)}updateAttachment(t){return"right"===t?"end":"left"===t?"start":t}_initializeOnDelegatedTarget(t,e){return e||this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return"function"==typeof t?t.call(this._element):t}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"onChange",enabled:!0,phase:"afterWrite",fn:t=>this._handlePopperPlacementChange(t)}],onFirstUpdate:t=>{t.options.placement!==t.placement&&this._handlePopperPlacementChange(t)}};return{...e,..."function"==typeof this._config.popperConfig?this._config.popperConfig(e):this._config.popperConfig}}_addAttachmentClass(t){this.getTipElement().classList.add(`${this._getBasicClassPrefix()}-${this.updateAttachment(t)}`)}_getAttachment(t){return Ji[t.toUpperCase()]}_setListeners(){this._config.trigger.split(" ").forEach((t=>{if("click"===t)j.on(this._element,this.constructor.Event.CLICK,this._config.selector,(t=>this.toggle(t)));else if("manual"!==t){const e=t===hn?this.constructor.Event.MOUSEENTER:this.constructor.Event.FOCUSIN,i=t===hn?this.constructor.Event.MOUSELEAVE:this.constructor.Event.FOCUSOUT;j.on(this._element,e,this._config.selector,(t=>this._enter(t))),j.on(this._element,i,this._config.selector,(t=>this._leave(t)))}})),this._hideModalHandler=()=>{this._element&&this.hide()},j.on(this._element.closest(ln),cn,this._hideModalHandler),this._config.selector?this._config={...this._config,trigger:"manual",selector:""}:this._fixTitle()}_fixTitle(){const t=this._element.getAttribute("title"),e=typeof this._element.getAttribute("data-bs-original-title");(t||"string"!==e)&&(this._element.setAttribute("data-bs-original-title",t||""),!t||this._element.getAttribute("aria-label")||this._element.textContent||this._element.setAttribute("aria-label",t),this._element.setAttribute("title",""))}_enter(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusin"===t.type?dn:hn]=!0),e.getTipElement().classList.contains(sn)||e._hoverState===on?e._hoverState=on:(clearTimeout(e._timeout),e._hoverState=on,e._config.delay&&e._config.delay.show?e._timeout=setTimeout((()=>{e._hoverState===on&&e.show()}),e._config.delay.show):e.show())}_leave(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusout"===t.type?dn:hn]=e._element.contains(t.relatedTarget)),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=rn,e._config.delay&&e._config.delay.hide?e._timeout=setTimeout((()=>{e._hoverState===rn&&e.hide()}),e._config.delay.hide):e.hide())}_isWithActiveTrigger(){for(const t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1}_getConfig(t){const e=U.getDataAttributes(this._element);return Object.keys(e).forEach((t=>{Gi.has(t)&&delete e[t]})),(t={...this.constructor.Default,...e,..."object"==typeof t&&t?t:{}}).container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),a(Qi,t,this.constructor.DefaultType),t.sanitize&&(t.template=Yi(t.template,t.allowList,t.sanitizeFn)),t}_getDelegateConfig(){const t={};for(const e in this._config)this.constructor.Default[e]!==this._config[e]&&(t[e]=this._config[e]);return t}_cleanTipClass(){const t=this.getTipElement(),e=new RegExp(`(^|\\s)${this._getBasicClassPrefix()}\\S+`,"g"),i=t.getAttribute("class").match(e);null!==i&&i.length>0&&i.map((t=>t.trim())).forEach((e=>t.classList.remove(e)))}_getBasicClassPrefix(){return"bs-tooltip"}_handlePopperPlacementChange(t){const{state:e}=t;e&&(this.tip=e.elements.popper,this._cleanTipClass(),this._addAttachmentClass(this._getAttachment(e.placement)))}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null)}static jQueryInterface(t){return this.each((function(){const e=un.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}g(un);const fn={...un.Default,placement:"right",offset:[0,8],trigger:"click",content:"",template:''},pn={...un.DefaultType,content:"(string|element|function)"},mn={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"};class gn extends un{static get Default(){return fn}static get NAME(){return"popover"}static get Event(){return mn}static get DefaultType(){return pn}isWithContent(){return this.getTitle()||this._getContent()}setContent(t){this._sanitizeAndSetContent(t,this.getTitle(),".popover-header"),this._sanitizeAndSetContent(t,this._getContent(),".popover-body")}_getContent(){return this._resolvePossibleFunction(this._config.content)}_getBasicClassPrefix(){return"bs-popover"}static jQueryInterface(t){return this.each((function(){const e=gn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}g(gn);const _n="scrollspy",bn={offset:10,method:"auto",target:""},vn={offset:"number",method:"string",target:"(string|element)"},yn="active",wn=".nav-link, .list-group-item, .dropdown-item",En="position";class An extends B{constructor(t,e){super(t),this._scrollElement="BODY"===this._element.tagName?window:this._element,this._config=this._getConfig(e),this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,j.on(this._scrollElement,"scroll.bs.scrollspy",(()=>this._process())),this.refresh(),this._process()}static get Default(){return bn}static get NAME(){return _n}refresh(){const t=this._scrollElement===this._scrollElement.window?"offset":En,e="auto"===this._config.method?t:this._config.method,n=e===En?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),V.find(wn,this._config.target).map((t=>{const s=i(t),o=s?V.findOne(s):null;if(o){const t=o.getBoundingClientRect();if(t.width||t.height)return[U[e](o).top+n,s]}return null})).filter((t=>t)).sort(((t,e)=>t[0]-e[0])).forEach((t=>{this._offsets.push(t[0]),this._targets.push(t[1])}))}dispose(){j.off(this._scrollElement,".bs.scrollspy"),super.dispose()}_getConfig(t){return(t={...bn,...U.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}}).target=r(t.target)||document.documentElement,a(_n,t,vn),t}_getScrollTop(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop}_getScrollHeight(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)}_getOffsetHeight(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height}_process(){const t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),i=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=i){const t=this._targets[this._targets.length-1];this._activeTarget!==t&&this._activate(t)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(let e=this._offsets.length;e--;)this._activeTarget!==this._targets[e]&&t>=this._offsets[e]&&(void 0===this._offsets[e+1]||t`${e}[data-bs-target="${t}"],${e}[href="${t}"]`)),i=V.findOne(e.join(","),this._config.target);i.classList.add(yn),i.classList.contains("dropdown-item")?V.findOne(".dropdown-toggle",i.closest(".dropdown")).classList.add(yn):V.parents(i,".nav, .list-group").forEach((t=>{V.prev(t,".nav-link, .list-group-item").forEach((t=>t.classList.add(yn))),V.prev(t,".nav-item").forEach((t=>{V.children(t,".nav-link").forEach((t=>t.classList.add(yn)))}))})),j.trigger(this._scrollElement,"activate.bs.scrollspy",{relatedTarget:t})}_clear(){V.find(wn,this._config.target).filter((t=>t.classList.contains(yn))).forEach((t=>t.classList.remove(yn)))}static jQueryInterface(t){return this.each((function(){const e=An.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}j.on(window,"load.bs.scrollspy.data-api",(()=>{V.find('[data-bs-spy="scroll"]').forEach((t=>new An(t)))})),g(An);const Tn="active",On="fade",Cn="show",kn=".active",Ln=":scope > li > .active";class xn extends B{static get NAME(){return"tab"}show(){if(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&this._element.classList.contains(Tn))return;let t;const e=n(this._element),i=this._element.closest(".nav, .list-group");if(i){const e="UL"===i.nodeName||"OL"===i.nodeName?Ln:kn;t=V.find(e,i),t=t[t.length-1]}const s=t?j.trigger(t,"hide.bs.tab",{relatedTarget:this._element}):null;if(j.trigger(this._element,"show.bs.tab",{relatedTarget:t}).defaultPrevented||null!==s&&s.defaultPrevented)return;this._activate(this._element,i);const o=()=>{j.trigger(t,"hidden.bs.tab",{relatedTarget:this._element}),j.trigger(this._element,"shown.bs.tab",{relatedTarget:t})};e?this._activate(e,e.parentNode,o):o()}_activate(t,e,i){const n=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?V.children(e,kn):V.find(Ln,e))[0],s=i&&n&&n.classList.contains(On),o=()=>this._transitionComplete(t,n,i);n&&s?(n.classList.remove(Cn),this._queueCallback(o,t,!0)):o()}_transitionComplete(t,e,i){if(e){e.classList.remove(Tn);const t=V.findOne(":scope > .dropdown-menu .active",e.parentNode);t&&t.classList.remove(Tn),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}t.classList.add(Tn),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),u(t),t.classList.contains(On)&&t.classList.add(Cn);let n=t.parentNode;if(n&&"LI"===n.nodeName&&(n=n.parentNode),n&&n.classList.contains("dropdown-menu")){const e=t.closest(".dropdown");e&&V.find(".dropdown-toggle",e).forEach((t=>t.classList.add(Tn))),t.setAttribute("aria-expanded",!0)}i&&i()}static jQueryInterface(t){return this.each((function(){const e=xn.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}j.on(document,"click.bs.tab.data-api",'[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),c(this)||xn.getOrCreateInstance(this).show()})),g(xn);const Dn="toast",Sn="hide",Nn="show",In="showing",Pn={animation:"boolean",autohide:"boolean",delay:"number"},jn={animation:!0,autohide:!0,delay:5e3};class Mn extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get DefaultType(){return Pn}static get Default(){return jn}static get NAME(){return Dn}show(){j.trigger(this._element,"show.bs.toast").defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(Sn),u(this._element),this._element.classList.add(Nn),this._element.classList.add(In),this._queueCallback((()=>{this._element.classList.remove(In),j.trigger(this._element,"shown.bs.toast"),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this._element.classList.contains(Nn)&&(j.trigger(this._element,"hide.bs.toast").defaultPrevented||(this._element.classList.add(In),this._queueCallback((()=>{this._element.classList.add(Sn),this._element.classList.remove(In),this._element.classList.remove(Nn),j.trigger(this._element,"hidden.bs.toast")}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this._element.classList.contains(Nn)&&this._element.classList.remove(Nn),super.dispose()}_getConfig(t){return t={...jn,...U.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}},a(Dn,t,this.constructor.DefaultType),t}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){j.on(this._element,"mouseover.bs.toast",(t=>this._onInteraction(t,!0))),j.on(this._element,"mouseout.bs.toast",(t=>this._onInteraction(t,!1))),j.on(this._element,"focusin.bs.toast",(t=>this._onInteraction(t,!0))),j.on(this._element,"focusout.bs.toast",(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=Mn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(Mn),g(Mn),{Alert:W,Button:z,Carousel:st,Collapse:pt,Dropdown:hi,Modal:Hi,Offcanvas:Fi,Popover:gn,ScrollSpy:An,Tab:xn,Toast:Mn,Tooltip:un}})); +//# sourceMappingURL=bootstrap.bundle.min.js.map \ No newline at end of file diff --git a/mailcow/data/web/js/build/002-slider.min.js b/mailcow/data/web/js/build/002-slider.min.js new file mode 100644 index 0000000..f04ef59 --- /dev/null +++ b/mailcow/data/web/js/build/002-slider.min.js @@ -0,0 +1 @@ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).noUiSlider={})}(this,function(st){"use strict";var t,e;function n(t){return"object"==typeof t&&"function"==typeof t.to&&"function"==typeof t.from}function at(t){t.parentElement.removeChild(t)}function lt(t){return null!=t}function ut(t){t.preventDefault()}function o(t){return"number"==typeof t&&!isNaN(t)&&isFinite(t)}function ct(t,e,r){0=e[r];)r+=1;return r}function i(t,e,r){if(r>=t.slice(-1)[0])return 100;var n,i,o=f(r,t),s=t[o-1],a=t[o],l=e[o-1],u=e[o];return l+(i=r,p(n=[s,a],n[0]<0?i+Math.abs(n[0]):i-n[0],0)/c(l,u))}function s(t,e,r,n){if(100===n)return n;var i,o,s=f(n,t),a=t[s-1],l=t[s];return r?(l-a)/2this.xPct[i+1];)i++;else t===this.xPct[this.xPct.length-1]&&(i=this.xPct.length-2);r||t!==this.xPct[i+1]||i++,null===e&&(e=[]);var o=1,s=e[i],a=0,l=0,u=0,c=0;for(n=r?(t-this.xPct[i])/(this.xPct[i+1]-this.xPct[i]):(this.xPct[i+1]-t)/(this.xPct[i+1]-this.xPct[i]);0= 2) required for mode 'count'.");for(var e=t.values-1,r=100/e,n=[];e--;)n[e]=e*r;return n.push(100),D(n,t.stepped)}(m),v={},t=y.xVal[0],e=y.xVal[y.xVal.length-1],b=!1,S=!1,x=0;return(g=g.slice().sort(function(t,e){return t-e}).filter(function(t){return!this[t]&&(this[t]=!0)},{}))[0]!==t&&(g.unshift(t),b=!0),g[g.length-1]!==e&&(g.push(e),S=!0),g.forEach(function(t,e){var r,n,i,o,s,a,l,u,c,p,f=t,d=g[e+1],h=m.mode===st.PipsMode.Steps;for(h&&(r=y.xNumSteps[e]),r||(r=d-f),void 0===d&&(d=f),r=Math.max(r,1e-7),n=f;n<=d;n=Number((n+r).toFixed(7))){for(u=(s=(o=y.toStepping(n))-x)/(m.density||1),p=s/(c=Math.round(u)),i=1;i<=c;i+=1)v[(a=x+i*p).toFixed(5)]=[y.fromStepping(a),0];l=-1st.PipsType.NoValue&&((n=C(s,!1)).className=p(r,b.cssClasses.value),n.setAttribute("data-value",String(e)),n.style[b.style]=t+"%",n.innerHTML=String(o.to(e)))}}(t,e[t][0],e[t][1])}),s}function O(){i&&(at(i),i=null)}function j(t){O();var e=L(t),r=t.filter,n=t.format||{to:function(t){return String(Math.round(t))},from:Number};return i=d.appendChild(T(e,r,n))}function z(){var t=l.getBoundingClientRect(),e="offset"+["Width","Height"][b.ort];return 0===b.ort?t.width||l[e]:t.height||l[e]}function H(i,o,s,a){var e=function(t){var e,r,n=function(r,t,n){var e=0===r.type.indexOf("touch"),i=0===r.type.indexOf("mouse"),o=0===r.type.indexOf("pointer"),s=0,a=0;0===r.type.indexOf("MSPointer")&&(o=!0);if("mousedown"===r.type&&!r.buttons&&!r.touches)return!1;if(e){var l=function(t){var e=t.target;return e===n||n.contains(e)||r.composed&&r.composedPath().shift()===n};if("touchstart"===r.type){var u=Array.prototype.filter.call(r.touches,l);if(1r.stepAfter.startValue&&(i=r.stepAfter.startValue-n),o=n>r.thisStep.startValue?r.thisStep.step:!1!==r.stepBefore.step&&n-r.stepBefore.highestStep,100===e?i=null:0===e&&(o=null);var s=y.countStepDecimals();return null!==i&&!1!==i&&(i=Number(i.toFixed(s))),null!==o&&!1!==o&&(o=Number(o.toFixed(s))),[o,i]}dt(e=d,b.cssClasses.target),0===b.dir?dt(e,b.cssClasses.ltr):dt(e,b.cssClasses.rtl),0===b.ort?dt(e,b.cssClasses.horizontal):dt(e,b.cssClasses.vertical),dt(e,"rtl"===getComputedStyle(e).direction?b.cssClasses.textDirectionRtl:b.cssClasses.textDirectionLtr),l=C(e,b.cssClasses.base),function(t,e){var r=C(e,b.cssClasses.connects);u=[],(s=[]).push(V(r,t[0]));for(var n=0;n + if (!String.prototype.startsWith) { + (function () { + 'use strict'; // needed to support `apply`/`call` with `undefined`/`null` + var toString = {}.toString; + var startsWith = function (search) { + if (this == null) { + throw new TypeError(); + } + var string = String(this); + if (search && toString.call(search) == '[object RegExp]') { + throw new TypeError(); + } + var stringLength = string.length; + var searchString = String(search); + var searchLength = searchString.length; + var position = arguments.length > 1 ? arguments[1] : undefined; + // `ToInteger` + var pos = position ? Number(position) : 0; + if (pos != pos) { // better `isNaN` + pos = 0; + } + var start = Math.min(Math.max(pos, 0), stringLength); + // Avoid the `indexOf` call if no match is possible + if (searchLength + start > stringLength) { + return false; + } + var index = -1; + while (++index < searchLength) { + if (string.charCodeAt(start + index) != searchString.charCodeAt(index)) { + return false; + } + } + return true; + }; + if (Object.defineProperty) { + Object.defineProperty(String.prototype, 'startsWith', { + 'value': startsWith, + 'configurable': true, + 'writable': true + }); + } else { + String.prototype.startsWith = startsWith; + } + }()); + } + + function toKebabCase (str) { + return str.replace(/[A-Z]+(?![a-z])|[A-Z]/g, function ($, ofs) { + return (ofs ? '-' : '') + $.toLowerCase(); + }); + } + + function getSelectedOptions () { + var options = this.selectpicker.main.data; + + if (this.options.source.data || this.options.source.search) { + options = Object.values(this.selectpicker.optionValuesDataMap); + } + + var selectedOptions = options.filter(function (item) { + if (item.selected) { + if (this.options.hideDisabled && item.disabled) return false; + return true; + } + + return false; + }, this); + + // ensure only 1 option is selected if multiple are set in the data source + if (this.options.source.data && !this.multiple && selectedOptions.length > 1) { + for (var i = 0; i < selectedOptions.length - 1; i++) { + selectedOptions[i].selected = false; + } + + selectedOptions = [ selectedOptions[selectedOptions.length - 1] ]; + } + + return selectedOptions; + } + + // much faster than $.val() + function getSelectValues (selectedOptions) { + var value = [], + options = selectedOptions || getSelectedOptions.call(this), + opt; + + for (var i = 0, len = options.length; i < len; i++) { + opt = options[i]; + + if (!opt.disabled) { + value.push(opt.value === undefined ? opt.text : opt.value); + } + } + + if (!this.multiple) { + return !value.length ? null : value[0]; + } + + return value; + } + + // set data-selected on select element if the value has been programmatically selected + // prior to initialization of bootstrap-select + // * consider removing or replacing an alternative method * + var valHooks = { + useDefault: false, + _set: $.valHooks.select.set + }; + + $.valHooks.select.set = function (elem, value) { + if (value && !valHooks.useDefault) $(elem).data('selected', true); + + return valHooks._set.apply(this, arguments); + }; + + var changedArguments = null; + + var EventIsSupported = (function () { + try { + new Event('change'); + return true; + } catch (e) { + return false; + } + })(); + + $.fn.triggerNative = function (eventName) { + var el = this[0], + event; + + if (el.dispatchEvent) { // for modern browsers & IE9+ + if (EventIsSupported) { + // For modern browsers + event = new Event(eventName, { + bubbles: true + }); + } else { + // For IE since it doesn't support Event constructor + event = document.createEvent('Event'); + event.initEvent(eventName, true, false); + } + + el.dispatchEvent(event); + } + }; + // + + function stringSearch (li, searchString, method, normalize) { + var stringTypes = [ + 'display', + 'subtext', + 'tokens' + ], + searchSuccess = false; + + for (var i = 0; i < stringTypes.length; i++) { + var stringType = stringTypes[i], + string = li[stringType]; + + if (string) { + string = string.toString(); + + // Strip HTML tags. This isn't perfect, but it's much faster than any other method + if (stringType === 'display') { + string = string.replace(/<[^>]+>/g, ''); + } + + if (normalize) string = normalizeToBase(string); + string = string.toUpperCase(); + + if (typeof method === 'function') { + searchSuccess = method(string, searchString); + } else if (method === 'contains') { + searchSuccess = string.indexOf(searchString) >= 0; + } else { + searchSuccess = string.startsWith(searchString); + } + + if (searchSuccess) break; + } + } + + return searchSuccess; + } + + function toInteger (value) { + return parseInt(value, 10) || 0; + } + + // Borrowed from Lodash (_.deburr) + /** Used to map Latin Unicode letters to basic Latin letters. */ + var deburredLetters = { + // Latin-1 Supplement block. + '\xc0': 'A', '\xc1': 'A', '\xc2': 'A', '\xc3': 'A', '\xc4': 'A', '\xc5': 'A', + '\xe0': 'a', '\xe1': 'a', '\xe2': 'a', '\xe3': 'a', '\xe4': 'a', '\xe5': 'a', + '\xc7': 'C', '\xe7': 'c', + '\xd0': 'D', '\xf0': 'd', + '\xc8': 'E', '\xc9': 'E', '\xca': 'E', '\xcb': 'E', + '\xe8': 'e', '\xe9': 'e', '\xea': 'e', '\xeb': 'e', + '\xcc': 'I', '\xcd': 'I', '\xce': 'I', '\xcf': 'I', + '\xec': 'i', '\xed': 'i', '\xee': 'i', '\xef': 'i', + '\xd1': 'N', '\xf1': 'n', + '\xd2': 'O', '\xd3': 'O', '\xd4': 'O', '\xd5': 'O', '\xd6': 'O', '\xd8': 'O', + '\xf2': 'o', '\xf3': 'o', '\xf4': 'o', '\xf5': 'o', '\xf6': 'o', '\xf8': 'o', + '\xd9': 'U', '\xda': 'U', '\xdb': 'U', '\xdc': 'U', + '\xf9': 'u', '\xfa': 'u', '\xfb': 'u', '\xfc': 'u', + '\xdd': 'Y', '\xfd': 'y', '\xff': 'y', + '\xc6': 'Ae', '\xe6': 'ae', + '\xde': 'Th', '\xfe': 'th', + '\xdf': 'ss', + // Latin Extended-A block. + '\u0100': 'A', '\u0102': 'A', '\u0104': 'A', + '\u0101': 'a', '\u0103': 'a', '\u0105': 'a', + '\u0106': 'C', '\u0108': 'C', '\u010a': 'C', '\u010c': 'C', + '\u0107': 'c', '\u0109': 'c', '\u010b': 'c', '\u010d': 'c', + '\u010e': 'D', '\u0110': 'D', '\u010f': 'd', '\u0111': 'd', + '\u0112': 'E', '\u0114': 'E', '\u0116': 'E', '\u0118': 'E', '\u011a': 'E', + '\u0113': 'e', '\u0115': 'e', '\u0117': 'e', '\u0119': 'e', '\u011b': 'e', + '\u011c': 'G', '\u011e': 'G', '\u0120': 'G', '\u0122': 'G', + '\u011d': 'g', '\u011f': 'g', '\u0121': 'g', '\u0123': 'g', + '\u0124': 'H', '\u0126': 'H', '\u0125': 'h', '\u0127': 'h', + '\u0128': 'I', '\u012a': 'I', '\u012c': 'I', '\u012e': 'I', '\u0130': 'I', + '\u0129': 'i', '\u012b': 'i', '\u012d': 'i', '\u012f': 'i', '\u0131': 'i', + '\u0134': 'J', '\u0135': 'j', + '\u0136': 'K', '\u0137': 'k', '\u0138': 'k', + '\u0139': 'L', '\u013b': 'L', '\u013d': 'L', '\u013f': 'L', '\u0141': 'L', + '\u013a': 'l', '\u013c': 'l', '\u013e': 'l', '\u0140': 'l', '\u0142': 'l', + '\u0143': 'N', '\u0145': 'N', '\u0147': 'N', '\u014a': 'N', + '\u0144': 'n', '\u0146': 'n', '\u0148': 'n', '\u014b': 'n', + '\u014c': 'O', '\u014e': 'O', '\u0150': 'O', + '\u014d': 'o', '\u014f': 'o', '\u0151': 'o', + '\u0154': 'R', '\u0156': 'R', '\u0158': 'R', + '\u0155': 'r', '\u0157': 'r', '\u0159': 'r', + '\u015a': 'S', '\u015c': 'S', '\u015e': 'S', '\u0160': 'S', + '\u015b': 's', '\u015d': 's', '\u015f': 's', '\u0161': 's', + '\u0162': 'T', '\u0164': 'T', '\u0166': 'T', + '\u0163': 't', '\u0165': 't', '\u0167': 't', + '\u0168': 'U', '\u016a': 'U', '\u016c': 'U', '\u016e': 'U', '\u0170': 'U', '\u0172': 'U', + '\u0169': 'u', '\u016b': 'u', '\u016d': 'u', '\u016f': 'u', '\u0171': 'u', '\u0173': 'u', + '\u0174': 'W', '\u0175': 'w', + '\u0176': 'Y', '\u0177': 'y', '\u0178': 'Y', + '\u0179': 'Z', '\u017b': 'Z', '\u017d': 'Z', + '\u017a': 'z', '\u017c': 'z', '\u017e': 'z', + '\u0132': 'IJ', '\u0133': 'ij', + '\u0152': 'Oe', '\u0153': 'oe', + '\u0149': "'n", '\u017f': 's' + }; + + /** Used to match Latin Unicode letters (excluding mathematical operators). */ + var reLatin = /[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g; + + /** Used to compose unicode character classes. */ + var rsComboMarksRange = '\\u0300-\\u036f', + reComboHalfMarksRange = '\\ufe20-\\ufe2f', + rsComboSymbolsRange = '\\u20d0-\\u20ff', + rsComboMarksExtendedRange = '\\u1ab0-\\u1aff', + rsComboMarksSupplementRange = '\\u1dc0-\\u1dff', + rsComboRange = rsComboMarksRange + reComboHalfMarksRange + rsComboSymbolsRange + rsComboMarksExtendedRange + rsComboMarksSupplementRange; + + /** Used to compose unicode capture groups. */ + var rsCombo = '[' + rsComboRange + ']'; + + /** + * Used to match [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks) and + * [combining diacritical marks for symbols](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks_for_Symbols). + */ + var reComboMark = RegExp(rsCombo, 'g'); + + function deburrLetter (key) { + return deburredLetters[key]; + }; + + function normalizeToBase (string) { + string = string.toString(); + return string && string.replace(reLatin, deburrLetter).replace(reComboMark, ''); + } + + // List of HTML entities for escaping. + var escapeMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`' + }; + + // Functions for escaping and unescaping strings to/from HTML interpolation. + var createEscaper = function (map) { + var escaper = function (match) { + return map[match]; + }; + // Regexes for identifying a key that needs to be escaped. + var source = '(?:' + Object.keys(map).join('|') + ')'; + var testRegexp = RegExp(source); + var replaceRegexp = RegExp(source, 'g'); + return function (string) { + string = string == null ? '' : '' + string; + return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; + }; + }; + + var htmlEscape = createEscaper(escapeMap); + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + + var keyCodeMap = { + 32: ' ', + 48: '0', + 49: '1', + 50: '2', + 51: '3', + 52: '4', + 53: '5', + 54: '6', + 55: '7', + 56: '8', + 57: '9', + 59: ';', + 65: 'A', + 66: 'B', + 67: 'C', + 68: 'D', + 69: 'E', + 70: 'F', + 71: 'G', + 72: 'H', + 73: 'I', + 74: 'J', + 75: 'K', + 76: 'L', + 77: 'M', + 78: 'N', + 79: 'O', + 80: 'P', + 81: 'Q', + 82: 'R', + 83: 'S', + 84: 'T', + 85: 'U', + 86: 'V', + 87: 'W', + 88: 'X', + 89: 'Y', + 90: 'Z', + 96: '0', + 97: '1', + 98: '2', + 99: '3', + 100: '4', + 101: '5', + 102: '6', + 103: '7', + 104: '8', + 105: '9' + }; + + var keyCodes = { + ESCAPE: 27, // KeyboardEvent.which value for Escape (Esc) key + ENTER: 13, // KeyboardEvent.which value for Enter key + SPACE: 32, // KeyboardEvent.which value for space key + TAB: 9, // KeyboardEvent.which value for tab key + ARROW_UP: 38, // KeyboardEvent.which value for up arrow key + ARROW_DOWN: 40 // KeyboardEvent.which value for down arrow key + }; + + // eslint-disable-next-line no-undef + var Dropdown = window.Dropdown || bootstrap.Dropdown; + + function getVersion () { + var version; + + try { + version = $.fn.dropdown.Constructor.VERSION; + } catch (err) { + version = Dropdown.VERSION; + } + + return version; + } + + var version = { + success: false, + major: '3' + }; + + try { + version.full = (getVersion() || '').split(' ')[0].split('.'); + version.major = version.full[0]; + version.success = true; + } catch (err) { + // do nothing + } + + var selectId = 0; + + var EVENT_KEY = '.bs.select'; + + var classNames = { + DISABLED: 'disabled', + DIVIDER: 'divider', + SHOW: 'open', + DROPUP: 'dropup', + MENU: 'dropdown-menu', + MENURIGHT: 'dropdown-menu-right', + MENULEFT: 'dropdown-menu-left', + // to-do: replace with more advanced template/customization options + BUTTONCLASS: 'btn-secondary', + POPOVERHEADER: 'popover-title', + ICONBASE: 'glyphicon', + TICKICON: 'glyphicon-ok' + }; + + var Selector = { + MENU: '.' + classNames.MENU, + DATA_TOGGLE: 'data-toggle="dropdown"' + }; + + var elementTemplates = { + div: document.createElement('div'), + span: document.createElement('span'), + i: document.createElement('i'), + subtext: document.createElement('small'), + a: document.createElement('a'), + li: document.createElement('li'), + whitespace: document.createTextNode('\u00A0'), + fragment: document.createDocumentFragment(), + option: document.createElement('option') + }; + + elementTemplates.selectedOption = elementTemplates.option.cloneNode(false); + elementTemplates.selectedOption.setAttribute('selected', true); + + elementTemplates.noResults = elementTemplates.li.cloneNode(false); + elementTemplates.noResults.className = 'no-results'; + + elementTemplates.a.setAttribute('role', 'option'); + elementTemplates.a.className = 'dropdown-item'; + + elementTemplates.subtext.className = 'text-muted'; + + elementTemplates.text = elementTemplates.span.cloneNode(false); + elementTemplates.text.className = 'text'; + + elementTemplates.checkMark = elementTemplates.span.cloneNode(false); + + var REGEXP_ARROW = new RegExp(keyCodes.ARROW_UP + '|' + keyCodes.ARROW_DOWN); + var REGEXP_TAB_OR_ESCAPE = new RegExp('^' + keyCodes.TAB + '$|' + keyCodes.ESCAPE); + + var generateOption = { + li: function (content, classes, optgroup) { + var li = elementTemplates.li.cloneNode(false); + + if (content) { + if (content.nodeType === 1 || content.nodeType === 11) { + li.appendChild(content); + } else { + li.innerHTML = content; + } + } + + if (typeof classes !== 'undefined' && classes !== '') li.className = classes; + if (typeof optgroup !== 'undefined' && optgroup !== null) li.classList.add('optgroup-' + optgroup); + + return li; + }, + + a: function (text, classes, inline) { + var a = elementTemplates.a.cloneNode(true); + + if (text) { + if (text.nodeType === 11) { + a.appendChild(text); + } else { + a.insertAdjacentHTML('beforeend', text); + } + } + + if (typeof classes !== 'undefined' && classes !== '') a.classList.add.apply(a.classList, classes.split(/\s+/)); + if (inline) a.setAttribute('style', inline); + + return a; + }, + + text: function (options, useFragment) { + var textElement = elementTemplates.text.cloneNode(false), + subtextElement, + iconElement; + + if (options.content) { + textElement.innerHTML = options.content; + } else { + textElement.textContent = options.text; + + if (options.icon) { + var whitespace = elementTemplates.whitespace.cloneNode(false); + + // need to use for icons in the button to prevent a breaking change + // note: switch to span in next major release + iconElement = (useFragment === true ? elementTemplates.i : elementTemplates.span).cloneNode(false); + iconElement.className = this.options.iconBase + ' ' + options.icon; + + elementTemplates.fragment.appendChild(iconElement); + elementTemplates.fragment.appendChild(whitespace); + } + + if (options.subtext) { + subtextElement = elementTemplates.subtext.cloneNode(false); + subtextElement.textContent = options.subtext; + textElement.appendChild(subtextElement); + } + } + + if (useFragment === true) { + while (textElement.childNodes.length > 0) { + elementTemplates.fragment.appendChild(textElement.childNodes[0]); + } + } else { + elementTemplates.fragment.appendChild(textElement); + } + + return elementTemplates.fragment; + }, + + label: function (options) { + var textElement = elementTemplates.text.cloneNode(false), + subtextElement, + iconElement; + + textElement.innerHTML = options.display; + + if (options.icon) { + var whitespace = elementTemplates.whitespace.cloneNode(false); + + iconElement = elementTemplates.span.cloneNode(false); + iconElement.className = this.options.iconBase + ' ' + options.icon; + + elementTemplates.fragment.appendChild(iconElement); + elementTemplates.fragment.appendChild(whitespace); + } + + if (options.subtext) { + subtextElement = elementTemplates.subtext.cloneNode(false); + subtextElement.textContent = options.subtext; + textElement.appendChild(subtextElement); + } + + elementTemplates.fragment.appendChild(textElement); + + return elementTemplates.fragment; + } + }; + + var getOptionData = { + fromOption: function (option, type) { + var value; + + switch (type) { + case 'divider': + value = option.getAttribute('data-divider') === 'true'; + break; + + case 'text': + value = option.textContent; + break; + + case 'label': + value = option.label; + break; + + case 'style': + value = option.style.cssText; + break; + + case 'title': + value = option.title; + break; + + default: + value = option.getAttribute('data-' + toKebabCase(type)); + break; + } + + return value; + }, + fromDataSource: function (option, type) { + var value; + + switch (type) { + case 'text': + case 'label': + value = option.text || option.value || ''; + break; + + default: + value = option[type]; + break; + } + + return value; + } + }; + + function showNoResults (searchMatch, searchValue) { + if (!searchMatch.length) { + elementTemplates.noResults.innerHTML = this.options.noneResultsText.replace('{0}', '"' + htmlEscape(searchValue) + '"'); + this.$menuInner[0].firstChild.appendChild(elementTemplates.noResults); + } + } + + function filterHidden (item) { + return !(item.hidden || this.options.hideDisabled && item.disabled); + } + + var Selectpicker = function (element, options) { + var that = this; + + // bootstrap-select has been initialized - revert valHooks.select.set back to its original function + if (!valHooks.useDefault) { + $.valHooks.select.set = valHooks._set; + valHooks.useDefault = true; + } + + this.$element = $(element); + this.$newElement = null; + this.$button = null; + this.$menu = null; + this.options = options; + this.selectpicker = { + main: { + data: [], + optionQueue: elementTemplates.fragment.cloneNode(false), + hasMore: false + }, + search: { + data: [], + hasMore: false + }, + current: {}, // current is either equal to main or search depending on if a search is in progress + view: {}, + // map of option values and their respective data (only used in conjunction with options.source) + optionValuesDataMap: {}, + isSearching: false, + keydown: { + keyHistory: '', + resetKeyHistory: { + start: function () { + return setTimeout(function () { + that.selectpicker.keydown.keyHistory = ''; + }, 800); + } + } + } + }; + + this.sizeInfo = {}; + + // Format window padding + var winPad = this.options.windowPadding; + if (typeof winPad === 'number') { + this.options.windowPadding = [winPad, winPad, winPad, winPad]; + } + + // Expose public methods + this.val = Selectpicker.prototype.val; + this.render = Selectpicker.prototype.render; + this.refresh = Selectpicker.prototype.refresh; + this.setStyle = Selectpicker.prototype.setStyle; + this.selectAll = Selectpicker.prototype.selectAll; + this.deselectAll = Selectpicker.prototype.deselectAll; + this.destroy = Selectpicker.prototype.destroy; + this.remove = Selectpicker.prototype.remove; + this.show = Selectpicker.prototype.show; + this.hide = Selectpicker.prototype.hide; + + this.init(); + }; + + Selectpicker.VERSION = '1.14.0-beta3'; + + // part of this is duplicated in i18n/defaults-en_US.js. Make sure to update both. + Selectpicker.DEFAULTS = { + noneSelectedText: 'Nothing selected', + noneResultsText: 'No results matched {0}', + countSelectedText: function (numSelected, numTotal) { + return (numSelected == 1) ? '{0} item selected' : '{0} items selected'; + }, + maxOptionsText: function (numAll, numGroup) { + return [ + (numAll == 1) ? 'Limit reached ({n} item max)' : 'Limit reached ({n} items max)', + (numGroup == 1) ? 'Group limit reached ({n} item max)' : 'Group limit reached ({n} items max)' + ]; + }, + selectAllText: 'Select All', + deselectAllText: 'Deselect All', + source: { + pageSize: 40 + }, + chunkSize: 40, + doneButton: false, + doneButtonText: 'Close', + multipleSeparator: ', ', + styleBase: 'btn', + style: classNames.BUTTONCLASS, + size: 'auto', + title: null, + placeholder: null, + allowClear: false, + selectedTextFormat: 'values', + width: false, + container: false, + hideDisabled: false, + showSubtext: false, + showIcon: true, + showContent: true, + dropupAuto: true, + header: false, + liveSearch: false, + liveSearchPlaceholder: null, + liveSearchNormalize: false, + liveSearchStyle: 'contains', + actionsBox: false, + iconBase: classNames.ICONBASE, + tickIcon: classNames.TICKICON, + showTick: false, + template: { + caret: '' + }, + maxOptions: false, + mobile: false, + selectOnTab: true, + dropdownAlignRight: false, + windowPadding: 0, + virtualScroll: 600, + display: false, + sanitize: true, + sanitizeFn: null, + whiteList: DefaultWhitelist + }; + + Selectpicker.prototype = { + + constructor: Selectpicker, + + init: function () { + var that = this, + id = this.$element.attr('id'), + element = this.$element[0], + form = element.form; + + selectId++; + this.selectId = 'bs-select-' + selectId; + + element.classList.add('bs-select-hidden'); + + this.multiple = this.$element.prop('multiple'); + this.autofocus = this.$element.prop('autofocus'); + + if (element.classList.contains('show-tick')) { + this.options.showTick = true; + } + + this.$newElement = this.createDropdown(); + + this.$element + .after(this.$newElement) + .prependTo(this.$newElement); + + // ensure select is associated with form element if it got unlinked after moving it inside newElement + if (form && element.form === null) { + if (!form.id) form.id = 'form-' + this.selectId; + element.setAttribute('form', form.id); + } + + this.$button = this.$newElement.children('button'); + if (this.options.allowClear) this.$clearButton = this.$button.children('.bs-select-clear-selected'); + this.$menu = this.$newElement.children(Selector.MENU); + this.$menuInner = this.$menu.children('.inner'); + this.$searchbox = this.$menu.find('input'); + + element.classList.remove('bs-select-hidden'); + + this.fetchData(function () { + that.render(true); + that.buildList(); + + requestAnimationFrame(function () { + that.$element.trigger('loaded' + EVENT_KEY); + }); + }); + + if (this.options.dropdownAlignRight === true) this.$menu[0].classList.add(classNames.MENURIGHT); + + if (typeof id !== 'undefined') { + this.$button.attr('data-id', id); + } + + this.checkDisabled(); + this.clickListener(); + + if (version.major > 4) this.dropdown = new Dropdown(this.$button[0]); + + if (this.options.liveSearch) { + this.liveSearchListener(); + this.focusedParent = this.$searchbox[0]; + } else { + this.focusedParent = this.$menuInner[0]; + } + + this.setStyle(); + this.setWidth(); + if (this.options.container) { + this.selectPosition(); + } else { + this.$element.on('hide' + EVENT_KEY, function () { + if (that.isVirtual()) { + // empty menu on close + var menuInner = that.$menuInner[0], + emptyMenu = menuInner.firstChild.cloneNode(false); + + // replace the existing UL with an empty one - this is faster than $.empty() or innerHTML = '' + menuInner.replaceChild(emptyMenu, menuInner.firstChild); + menuInner.scrollTop = 0; + } + }); + } + this.$menu.data('this', this); + this.$newElement.data('this', this); + if (this.options.mobile) this.mobile(); + + this.$newElement.on({ + 'hide.bs.dropdown': function (e) { + that.$element.trigger('hide' + EVENT_KEY, e); + }, + 'hidden.bs.dropdown': function (e) { + that.$element.trigger('hidden' + EVENT_KEY, e); + }, + 'show.bs.dropdown': function (e) { + that.$element.trigger('show' + EVENT_KEY, e); + }, + 'shown.bs.dropdown': function (e) { + that.$element.trigger('shown' + EVENT_KEY, e); + } + }); + + if (element.hasAttribute('required')) { + this.$element.on('invalid' + EVENT_KEY, function () { + that.$button[0].classList.add('bs-invalid'); + + that.$element + .on('shown' + EVENT_KEY + '.invalid', function () { + that.$element + .val(that.$element.val()) // set the value to hide the validation message in Chrome when menu is opened + .off('shown' + EVENT_KEY + '.invalid'); + }) + .on('rendered' + EVENT_KEY, function () { + // if select is no longer invalid, remove the bs-invalid class + if (this.validity.valid) that.$button[0].classList.remove('bs-invalid'); + that.$element.off('rendered' + EVENT_KEY); + }); + + that.$button.on('blur' + EVENT_KEY, function () { + that.$element.trigger('focus').trigger('blur'); + that.$button.off('blur' + EVENT_KEY); + }); + }); + } + + if (form) { + $(form).on('reset' + EVENT_KEY, function () { + requestAnimationFrame(function () { + that.render(); + }); + }); + } + }, + + createDropdown: function () { + // Options + // If we are multiple or showTick option is set, then add the show-tick class + var showTick = (this.multiple || this.options.showTick) ? ' show-tick' : '', + multiselectable = this.multiple ? ' aria-multiselectable="true"' : '', + inputGroup = '', + autofocus = this.autofocus ? ' autofocus' : ''; + + if (version.major < 4 && this.$element.parent().hasClass('input-group')) { + inputGroup = ' input-group-btn'; + } + + // Elements + var drop, + header = '', + searchbox = '', + actionsbox = '', + donebutton = '', + clearButton = ''; + + if (this.options.header) { + header = + '
    ' + + '' + + this.options.header + + '
    '; + } + + if (this.options.liveSearch) { + searchbox = + ''; + } + + if (this.multiple && this.options.actionsBox) { + actionsbox = + '
    ' + + '
    ' + + '' + + '' + + '
    ' + + '
    '; + } + + if (this.multiple && this.options.doneButton) { + donebutton = + '
    ' + + '
    ' + + '' + + '
    ' + + '
    '; + } + + if (this.options.allowClear) { + clearButton = '×'; + } + + drop = + ''; + + return $(drop); + }, + + setPositionData: function () { + this.selectpicker.view.canHighlight = []; + this.selectpicker.view.size = 0; + this.selectpicker.view.firstHighlightIndex = false; + + for (var i = 0; i < this.selectpicker.current.data.length; i++) { + var li = this.selectpicker.current.data[i], + canHighlight = true; + + if (li.type === 'divider') { + canHighlight = false; + li.height = this.sizeInfo.dividerHeight; + } else if (li.type === 'optgroup-label') { + canHighlight = false; + li.height = this.sizeInfo.dropdownHeaderHeight; + } else { + li.height = this.sizeInfo.liHeight; + } + + if (li.disabled) canHighlight = false; + + this.selectpicker.view.canHighlight.push(canHighlight); + + if (canHighlight) { + this.selectpicker.view.size++; + li.posinset = this.selectpicker.view.size; + if (this.selectpicker.view.firstHighlightIndex === false) this.selectpicker.view.firstHighlightIndex = i; + } + + li.position = (i === 0 ? 0 : this.selectpicker.current.data[i - 1].position) + li.height; + } + }, + + isVirtual: function () { + return (this.options.virtualScroll !== false) && (this.selectpicker.main.data.length >= this.options.virtualScroll) || this.options.virtualScroll === true; + }, + + createView: function (isSearching, setSize, refresh) { + var that = this, + scrollTop = 0; + + this.selectpicker.isSearching = isSearching; + this.selectpicker.current = isSearching ? this.selectpicker.search : this.selectpicker.main; + + this.setPositionData(); + + if (setSize) { + if (refresh) { + scrollTop = this.$menuInner[0].scrollTop; + } else if (!that.multiple) { + var element = that.$element[0], + selectedIndex = (element.options[element.selectedIndex] || {}).liIndex; + + if (typeof selectedIndex === 'number' && that.options.size !== false) { + var selectedData = that.selectpicker.main.data[selectedIndex], + position = selectedData && selectedData.position; + + if (position) { + scrollTop = position - ((that.sizeInfo.menuInnerHeight + that.sizeInfo.liHeight) / 2); + } + } + } + } + + scroll(scrollTop, true); + + this.$menuInner.off('scroll.createView').on('scroll.createView', function (e, updateValue) { + if (!that.noScroll) scroll(this.scrollTop, updateValue); + that.noScroll = false; + }); + + function scroll (scrollTop, init) { + var size = that.selectpicker.current.data.length, + chunks = [], + chunkSize, + chunkCount, + firstChunk, + lastChunk, + currentChunk, + prevPositions, + positionIsDifferent, + previousElements, + menuIsDifferent = true, + isVirtual = that.isVirtual(); + + that.selectpicker.view.scrollTop = scrollTop; + + chunkSize = that.options.chunkSize; // number of options in a chunk + chunkCount = Math.ceil(size / chunkSize) || 1; // number of chunks + + for (var i = 0; i < chunkCount; i++) { + var endOfChunk = (i + 1) * chunkSize; + + if (i === chunkCount - 1) { + endOfChunk = size; + } + + chunks[i] = [ + (i) * chunkSize + (!i ? 0 : 1), + endOfChunk + ]; + + if (!size) break; + + if (currentChunk === undefined && scrollTop - 1 <= that.selectpicker.current.data[endOfChunk - 1].position - that.sizeInfo.menuInnerHeight) { + currentChunk = i; + } + } + + if (currentChunk === undefined) currentChunk = 0; + + prevPositions = [that.selectpicker.view.position0, that.selectpicker.view.position1]; + + // always display previous, current, and next chunks + firstChunk = Math.max(0, currentChunk - 1); + lastChunk = Math.min(chunkCount - 1, currentChunk + 1); + + that.selectpicker.view.position0 = isVirtual === false ? 0 : (Math.max(0, chunks[firstChunk][0]) || 0); + that.selectpicker.view.position1 = isVirtual === false ? size : (Math.min(size, chunks[lastChunk][1]) || 0); + + positionIsDifferent = prevPositions[0] !== that.selectpicker.view.position0 || prevPositions[1] !== that.selectpicker.view.position1; + + if (that.activeElement !== undefined) { + if (init) { + if (that.activeElement !== that.selectedElement) { + that.defocusItem(that.activeElement); + } + that.activeElement = undefined; + } + + if (that.activeElement !== that.selectedElement) { + that.defocusItem(that.selectedElement); + } + } + + if (that.prevActiveElement !== undefined && that.prevActiveElement !== that.activeElement && that.prevActiveElement !== that.selectedElement) { + that.defocusItem(that.prevActiveElement); + } + + if (init || positionIsDifferent || that.selectpicker.current.hasMore) { + previousElements = that.selectpicker.view.visibleElements ? that.selectpicker.view.visibleElements.slice() : []; + + if (isVirtual === false) { + that.selectpicker.view.visibleElements = that.selectpicker.current.elements; + } else { + that.selectpicker.view.visibleElements = that.selectpicker.current.elements.slice(that.selectpicker.view.position0, that.selectpicker.view.position1); + } + + that.setOptionStatus(); + + // if searching, check to make sure the list has actually been updated before updating DOM + // this prevents unnecessary repaints + if (isSearching || (isVirtual === false && init)) menuIsDifferent = !isEqual(previousElements, that.selectpicker.view.visibleElements); + + // if virtual scroll is disabled and not searching, + // menu should never need to be updated more than once + if ((init || isVirtual === true) && menuIsDifferent) { + var menuInner = that.$menuInner[0], + menuFragment = document.createDocumentFragment(), + emptyMenu = menuInner.firstChild.cloneNode(false), + marginTop, + marginBottom, + elements = that.selectpicker.view.visibleElements, + toSanitize = []; + + // replace the existing UL with an empty one - this is faster than $.empty() + menuInner.replaceChild(emptyMenu, menuInner.firstChild); + + for (var i = 0, visibleElementsLen = elements.length; i < visibleElementsLen; i++) { + var element = elements[i], + elText, + elementData; + + if (that.options.sanitize) { + elText = element.lastChild; + + if (elText) { + elementData = that.selectpicker.current.data[i + that.selectpicker.view.position0]; + + if (elementData && elementData.content && !elementData.sanitized) { + toSanitize.push(elText); + elementData.sanitized = true; + } + } + } + + menuFragment.appendChild(element); + } + + if (that.options.sanitize && toSanitize.length) { + sanitizeHtml(toSanitize, that.options.whiteList, that.options.sanitizeFn); + } + + if (isVirtual === true) { + marginTop = (that.selectpicker.view.position0 === 0 ? 0 : that.selectpicker.current.data[that.selectpicker.view.position0 - 1].position); + marginBottom = (that.selectpicker.view.position1 > size - 1 ? 0 : that.selectpicker.current.data[size - 1].position - that.selectpicker.current.data[that.selectpicker.view.position1 - 1].position); + + menuInner.firstChild.style.marginTop = marginTop + 'px'; + menuInner.firstChild.style.marginBottom = marginBottom + 'px'; + } else { + menuInner.firstChild.style.marginTop = 0; + menuInner.firstChild.style.marginBottom = 0; + } + + menuInner.firstChild.appendChild(menuFragment); + + // if an option is encountered that is wider than the current menu width, update the menu width accordingly + // switch to ResizeObserver with increased browser support + if (isVirtual === true && that.sizeInfo.hasScrollBar) { + var menuInnerInnerWidth = menuInner.firstChild.offsetWidth; + + if (init && menuInnerInnerWidth < that.sizeInfo.menuInnerInnerWidth && that.sizeInfo.totalMenuWidth > that.sizeInfo.selectWidth) { + menuInner.firstChild.style.minWidth = that.sizeInfo.menuInnerInnerWidth + 'px'; + } else if (menuInnerInnerWidth > that.sizeInfo.menuInnerInnerWidth) { + // set to 0 to get actual width of menu + that.$menu[0].style.minWidth = 0; + + var actualMenuWidth = menuInner.firstChild.offsetWidth; + + if (actualMenuWidth > that.sizeInfo.menuInnerInnerWidth) { + that.sizeInfo.menuInnerInnerWidth = actualMenuWidth; + menuInner.firstChild.style.minWidth = that.sizeInfo.menuInnerInnerWidth + 'px'; + } + + // reset to default CSS styling + that.$menu[0].style.minWidth = ''; + } + } + } + + if ((!isSearching && that.options.source.data || isSearching && that.options.source.search) && that.selectpicker.current.hasMore && currentChunk === chunkCount - 1) { + // Don't load the next chunk until scrolling has started + // This prevents unnecessary requests while the user is typing if pageSize is <= chunkSize + if (scrollTop > 0) { + // Chunks use 0-based indexing, but pages use 1-based. Add 1 to convert and add 1 again to get next page + var page = Math.floor((currentChunk * that.options.chunkSize) / that.options.source.pageSize) + 2; + + that.fetchData(function () { + that.render(); + that.buildList(size, isSearching); + that.setPositionData(); + scroll(scrollTop); + }, isSearching ? 'search' : 'data', page, isSearching ? that.selectpicker.search.previousValue : undefined); + } + } + } + + that.prevActiveElement = that.activeElement; + + if (!that.options.liveSearch) { + that.$menuInner.trigger('focus'); + } else if (isSearching && init) { + var index = 0, + newActive; + + if (!that.selectpicker.view.canHighlight[index]) { + index = 1 + that.selectpicker.view.canHighlight.slice(1).indexOf(true); + } + + newActive = that.selectpicker.view.visibleElements[index]; + + that.defocusItem(that.selectpicker.view.currentActive); + + that.activeElement = (that.selectpicker.current.data[index] || {}).element; + + that.focusItem(newActive); + } + } + + $(window) + .off('resize' + EVENT_KEY + '.' + this.selectId + '.createView') + .on('resize' + EVENT_KEY + '.' + this.selectId + '.createView', function () { + var isActive = that.$newElement.hasClass(classNames.SHOW); + + if (isActive) scroll(that.$menuInner[0].scrollTop); + }); + }, + + focusItem: function (li, liData, noStyle) { + if (li) { + liData = liData || this.selectpicker.current.data[this.selectpicker.current.elements.indexOf(this.activeElement)]; + var a = li.firstChild; + + if (a) { + a.setAttribute('aria-setsize', this.selectpicker.view.size); + a.setAttribute('aria-posinset', liData.posinset); + + if (noStyle !== true) { + this.focusedParent.setAttribute('aria-activedescendant', a.id); + li.classList.add('active'); + a.classList.add('active'); + } + } + } + }, + + defocusItem: function (li) { + if (li) { + li.classList.remove('active'); + if (li.firstChild) li.firstChild.classList.remove('active'); + } + }, + + setPlaceholder: function () { + var that = this, + updateIndex = false; + + if ((this.options.placeholder || this.options.allowClear) && !this.multiple) { + if (!this.selectpicker.view.titleOption) this.selectpicker.view.titleOption = document.createElement('option'); + + // this option doesn't create a new
  • element, but does add a new option at the start, + // so startIndex should increase to prevent having to check every option for the bs-title-option class + updateIndex = true; + + var element = this.$element[0], + selectTitleOption = false, + titleNotAppended = !this.selectpicker.view.titleOption.parentNode, + selectedIndex = element.selectedIndex, + selectedOption = element.options[selectedIndex], + firstSelectable = element.querySelector('select > *:not(:disabled)'), + firstSelectableIndex = firstSelectable ? firstSelectable.index : 0, + navigation = window.performance && window.performance.getEntriesByType('navigation'), + // Safari doesn't support getEntriesByType('navigation') - fall back to performance.navigation + isNotBackForward = (navigation && navigation.length) ? navigation[0].type !== 'back_forward' : window.performance.navigation.type !== 2; + + if (titleNotAppended) { + // Use native JS to prepend option (faster) + this.selectpicker.view.titleOption.className = 'bs-title-option'; + this.selectpicker.view.titleOption.value = ''; + + // Check if selected or data-selected attribute is already set on an option. If not, select the titleOption option. + // the selected item may have been changed by user or programmatically before the bootstrap select plugin runs, + // if so, the select will have the data-selected attribute + selectTitleOption = !selectedOption || (selectedIndex === firstSelectableIndex && selectedOption.defaultSelected === false && this.$element.data('selected') === undefined); + } + + if (titleNotAppended || this.selectpicker.view.titleOption.index !== 0) { + element.insertBefore(this.selectpicker.view.titleOption, element.firstChild); + } + + // Set selected *after* appending to select, + // otherwise the option doesn't get selected in IE + // set using selectedIndex, as setting the selected attr to true here doesn't work in IE11 + if (selectTitleOption && isNotBackForward) { + element.selectedIndex = 0; + } else if (document.readyState !== 'complete') { + // if navigation type is back_forward, there's a chance the select will have its value set by BFCache + // wait for that value to be set, then run render again + window.addEventListener('pageshow', function () { + if (that.selectpicker.view.displayedValue !== element.value) that.render(); + }); + } + } + + return updateIndex; + }, + + fetchData: function (callback, type, page, searchValue) { + page = page || 1; + type = type || 'data'; + + var that = this, + data = this.options.source[type], + builtData; + + if (data) { + this.options.virtualScroll = true; + + if (typeof data === 'function') { + data.call( + this, + function (data, more, totalItems) { + var current = that.selectpicker[type === 'search' ? 'search' : 'main']; + current.hasMore = more; + current.totalItems = totalItems; + builtData = that.buildData(data, type); + callback.call(that, builtData); + that.$element.trigger('fetched' + EVENT_KEY); + }, + page, + searchValue + ); + } else if (Array.isArray(data)) { + builtData = that.buildData(data, type); + callback.call(that, builtData); + } + } else { + builtData = this.buildData(false, type); + callback.call(that, builtData); + } + }, + + buildData: function (data, type) { + var that = this; + var dataGetter = data === false ? getOptionData.fromOption : getOptionData.fromDataSource; + + var optionSelector = ':not([hidden]):not([data-hidden="true"]):not([style*="display: none"])', + mainData = [], + startLen = this.selectpicker.main.data ? this.selectpicker.main.data.length : 0, + optID = 0, + startIndex = this.setPlaceholder() && !data ? 1 : 0; // append the titleOption if necessary and skip the first option in the loop + + if (type === 'search') { + startLen = this.selectpicker.search.data.length; + } + + if (this.options.hideDisabled) optionSelector += ':not(:disabled)'; + + var selectOptions = data ? data.filter(filterHidden, this) : this.$element[0].querySelectorAll('select > *' + optionSelector); + + function addDivider (config) { + var previousData = mainData[mainData.length - 1]; + + // ensure optgroup doesn't create back-to-back dividers + if ( + previousData && + previousData.type === 'divider' && + (previousData.optID || config.optID) + ) { + return; + } + + config = config || {}; + config.type = 'divider'; + + mainData.push(config); + } + + function addOption (item, config) { + config = config || {}; + + config.divider = dataGetter(item, 'divider'); + + if (config.divider === true) { + addDivider({ + optID: config.optID + }); + } else { + var liIndex = mainData.length + startLen, + cssText = dataGetter(item, 'style'), + inlineStyle = cssText ? htmlEscape(cssText) : '', + optionClass = (item.className || '') + (config.optgroupClass || ''); + + if (config.optID) optionClass = 'opt ' + optionClass; + + config.optionClass = optionClass.trim(); + config.inlineStyle = inlineStyle; + + config.text = dataGetter(item, 'text'); + config.title = dataGetter(item, 'title'); + config.content = dataGetter(item, 'content'); + config.tokens = dataGetter(item, 'tokens'); + config.subtext = dataGetter(item, 'subtext'); + config.icon = dataGetter(item, 'icon'); + + config.display = config.content || config.text; + config.value = item.value === undefined ? item.text : item.value; + config.type = 'option'; + config.index = liIndex; + + config.option = !item.option ? item : item.option; // reference option element if it exists + config.option.liIndex = liIndex; + config.selected = !!item.selected; + config.disabled = config.disabled || !!item.disabled; + + if (data !== false) { + if (that.selectpicker.optionValuesDataMap[config.value]) { + config = $.extend(that.selectpicker.optionValuesDataMap[config.value], config); + } else { + that.selectpicker.optionValuesDataMap[config.value] = config; + } + } + + mainData.push(config); + } + } + + function addOptgroup (index, selectOptions) { + var optgroup = selectOptions[index], + // skip placeholder option + previous = index - 1 < startIndex ? false : selectOptions[index - 1], + next = selectOptions[index + 1], + options = data ? optgroup.children.filter(filterHidden, this) : optgroup.querySelectorAll('option' + optionSelector); + + if (!options.length) return; + + var config = { + display: htmlEscape(dataGetter(item, 'label')), + subtext: dataGetter(optgroup, 'subtext'), + icon: dataGetter(optgroup, 'icon'), + type: 'optgroup-label', + optgroupClass: ' ' + (optgroup.className || ''), + optgroup: optgroup + }, + headerIndex, + lastIndex; + + optID++; + + if (previous) { + addDivider({ optID: optID }); + } + + config.optID = optID; + + mainData.push(config); + + for (var j = 0, len = options.length; j < len; j++) { + var option = options[j]; + + if (j === 0) { + headerIndex = mainData.length - 1; + lastIndex = headerIndex + len; + } + + addOption(option, { + headerIndex: headerIndex, + lastIndex: lastIndex, + optID: config.optID, + optgroupClass: config.optgroupClass, + disabled: optgroup.disabled + }); + } + + if (next) { + addDivider({ optID: optID }); + } + } + + for (var len = selectOptions.length, i = startIndex; i < len; i++) { + var item = selectOptions[i], + children = item.children; + + if (children && children.length) { + addOptgroup.call(this, i, selectOptions); + } else { + addOption.call(this, item, {}); + } + } + + switch (type) { + case 'data': { + if (!this.selectpicker.main.data) { + this.selectpicker.main.data = []; + } + Array.prototype.push.apply(this.selectpicker.main.data, mainData); + this.selectpicker.current.data = this.selectpicker.main.data; + break; + } + case 'search': { + Array.prototype.push.apply(this.selectpicker.search.data, mainData); + break; + } + } + + return mainData; + }, + + buildList: function (size, searching) { + var that = this, + selectData = searching ? this.selectpicker.search.data : this.selectpicker.main.data, + mainElements = [], + widestOptionLength = 0; + + if ((that.options.showTick || that.multiple) && !elementTemplates.checkMark.parentNode) { + elementTemplates.checkMark.className = this.options.iconBase + ' ' + that.options.tickIcon + ' check-mark'; + elementTemplates.a.appendChild(elementTemplates.checkMark); + } + + function buildElement (mainElements, item) { + var liElement, + combinedLength = 0; + + switch (item.type) { + case 'divider': + liElement = generateOption.li( + false, + classNames.DIVIDER, + (item.optID ? item.optID + 'div' : undefined) + ); + + break; + + case 'option': + liElement = generateOption.li( + generateOption.a( + generateOption.text.call(that, item), + item.optionClass, + item.inlineStyle + ), + '', + item.optID + ); + + if (liElement.firstChild) { + liElement.firstChild.id = that.selectId + '-' + item.index; + } + + break; + + case 'optgroup-label': + liElement = generateOption.li( + generateOption.label.call(that, item), + 'dropdown-header' + item.optgroupClass, + item.optID + ); + + break; + } + + if (!item.element) { + item.element = liElement; + } else { + item.element.innerHTML = liElement.innerHTML; + } + mainElements.push(item.element); + + // count the number of characters in the option - not perfect, but should work in most cases + if (item.display) combinedLength += item.display.length; + if (item.subtext) combinedLength += item.subtext.length; + // if there is an icon, ensure this option's width is checked + if (item.icon) combinedLength += 1; + + if (combinedLength > widestOptionLength) { + widestOptionLength = combinedLength; + + // guess which option is the widest + // use this when calculating menu width + // not perfect, but it's fast, and the width will be updating accordingly when scrolling + that.selectpicker.view.widestOption = mainElements[mainElements.length - 1]; + } + } + + var startIndex = size || 0; + + for (var len = selectData.length, i = startIndex; i < len; i++) { + var item = selectData[i]; + + buildElement(mainElements, item); + } + + if (size) { + if (searching) { + Array.prototype.push.apply(this.selectpicker.search.elements, mainElements); + } else { + Array.prototype.push.apply(this.selectpicker.main.elements, mainElements); + this.selectpicker.current.elements = this.selectpicker.main.elements; + } + } else { + if (searching) { + this.selectpicker.search.elements = mainElements; + } else { + this.selectpicker.main.elements = this.selectpicker.current.elements = mainElements; + } + } + }, + + findLis: function () { + return this.$menuInner.find('.inner > li'); + }, + + render: function (init) { + var that = this, + element = this.$element[0], + // ensure titleOption is appended and selected (if necessary) before getting selectedOptions + placeholderSelected = this.setPlaceholder() && element.selectedIndex === 0, + selectedOptions = getSelectedOptions.call(this), + selectedCount = selectedOptions.length, + selectedValues = getSelectValues.call(this, selectedOptions), + button = this.$button[0], + buttonInner = button.querySelector('.filter-option-inner-inner'), + multipleSeparator = document.createTextNode(this.options.multipleSeparator), + titleFragment = elementTemplates.fragment.cloneNode(false), + showCount, + countMax, + hasContent = false; + + function createSelected (item) { + if (item.selected) { + that.createOption(item, true); + } else if (item.children && item.children.length) { + item.children.map(createSelected); + } + } + + // create selected option elements to ensure select value is correct + if (this.options.source.data && init) { + selectedOptions.map(createSelected); + element.appendChild(this.selectpicker.main.optionQueue); + + if (placeholderSelected) placeholderSelected = element.selectedIndex === 0; + } + + button.classList.toggle('bs-placeholder', that.multiple ? !selectedCount : !selectedValues && selectedValues !== 0); + + if (!that.multiple && selectedOptions.length === 1) { + that.selectpicker.view.displayedValue = selectedValues; + } + + if (this.options.selectedTextFormat === 'static') { + titleFragment = generateOption.text.call(this, { text: this.options.placeholder }, true); + } else { + showCount = this.multiple && this.options.selectedTextFormat.indexOf('count') !== -1 && selectedCount > 0; + + // determine if the number of selected options will be shown (showCount === true) + if (showCount) { + countMax = this.options.selectedTextFormat.split('>'); + showCount = (countMax.length > 1 && selectedCount > countMax[1]) || (countMax.length === 1 && selectedCount >= 2); + } + + // only loop through all selected options if the count won't be shown + if (showCount === false) { + if (!placeholderSelected) { + for (var selectedIndex = 0; selectedIndex < selectedCount; selectedIndex++) { + if (selectedIndex < 50) { + var option = selectedOptions[selectedIndex], + titleOptions = {}; + + if (option) { + if (this.multiple && selectedIndex > 0) { + titleFragment.appendChild(multipleSeparator.cloneNode(false)); + } + + if (option.title) { + titleOptions.text = option.title; + } else if (option.content && that.options.showContent) { + titleOptions.content = option.content.toString(); + hasContent = true; + } else { + if (that.options.showIcon) { + titleOptions.icon = option.icon; + } + if (that.options.showSubtext && !that.multiple && option.subtext) titleOptions.subtext = ' ' + option.subtext; + titleOptions.text = option.text.trim(); + } + + titleFragment.appendChild(generateOption.text.call(this, titleOptions, true)); + } + } else { + break; + } + } + + // add ellipsis + if (selectedCount > 49) { + titleFragment.appendChild(document.createTextNode('...')); + } + } + } else { + var optionSelector = ':not([hidden]):not([data-hidden="true"]):not([data-divider="true"]):not([style*="display: none"])'; + if (this.options.hideDisabled) optionSelector += ':not(:disabled)'; + + // If this is a multiselect, and selectedTextFormat is count, then show 1 of 2 selected, etc. + var totalCount = this.$element[0].querySelectorAll('select > option' + optionSelector + ', optgroup' + optionSelector + ' option' + optionSelector).length, + tr8nText = (typeof this.options.countSelectedText === 'function') ? this.options.countSelectedText(selectedCount, totalCount) : this.options.countSelectedText; + + titleFragment = generateOption.text.call(this, { + text: tr8nText.replace('{0}', selectedCount.toString()).replace('{1}', totalCount.toString()) + }, true); + } + } + + // If the select doesn't have a title, then use the default, or if nothing is set at all, use noneSelectedText + if (!titleFragment.childNodes.length) { + titleFragment = generateOption.text.call(this, { + text: this.options.placeholder ? this.options.placeholder : this.options.noneSelectedText + }, true); + } + + // if the select has a title, apply it to the button, and if not, apply titleFragment text + // strip all HTML tags and trim the result, then unescape any escaped tags + button.title = titleFragment.textContent.replace(/<[^>]*>?/g, '').trim(); + + if (this.options.sanitize && hasContent) { + sanitizeHtml([titleFragment], that.options.whiteList, that.options.sanitizeFn); + } + + buttonInner.innerHTML = ''; + buttonInner.appendChild(titleFragment); + + if (version.major < 4 && this.$newElement[0].classList.contains('bs3-has-addon')) { + var filterExpand = button.querySelector('.filter-expand'), + clone = buttonInner.cloneNode(true); + + clone.className = 'filter-expand'; + + if (filterExpand) { + button.replaceChild(clone, filterExpand); + } else { + button.appendChild(clone); + } + } + + this.$element.trigger('rendered' + EVENT_KEY); + }, + + /** + * @param [style] + * @param [status] + */ + setStyle: function (newStyle, status) { + var button = this.$button[0], + newElement = this.$newElement[0], + style = this.options.style.trim(), + buttonClass; + + if (this.$element.attr('class')) { + this.$newElement.addClass(this.$element.attr('class').replace(/selectpicker|mobile-device|bs-select-hidden|validate\[.*\]/gi, '')); + } + + if (version.major < 4) { + newElement.classList.add('bs3'); + + if (newElement.parentNode.classList && newElement.parentNode.classList.contains('input-group') && + (newElement.previousElementSibling || newElement.nextElementSibling) && + (newElement.previousElementSibling || newElement.nextElementSibling).classList.contains('input-group-addon') + ) { + newElement.classList.add('bs3-has-addon'); + } + } + + if (newStyle) { + buttonClass = newStyle.trim(); + } else { + buttonClass = style; + } + + if (status == 'add') { + if (buttonClass) button.classList.add.apply(button.classList, buttonClass.split(' ')); + } else if (status == 'remove') { + if (buttonClass) button.classList.remove.apply(button.classList, buttonClass.split(' ')); + } else { + if (style) button.classList.remove.apply(button.classList, style.split(' ')); + if (buttonClass) button.classList.add.apply(button.classList, buttonClass.split(' ')); + } + }, + + liHeight: function (refresh) { + if (!refresh && (this.options.size === false || Object.keys(this.sizeInfo).length)) return; + + var newElement = elementTemplates.div.cloneNode(false), + menu = elementTemplates.div.cloneNode(false), + menuInner = elementTemplates.div.cloneNode(false), + menuInnerInner = document.createElement('ul'), + divider = elementTemplates.li.cloneNode(false), + dropdownHeader = elementTemplates.li.cloneNode(false), + li, + a = elementTemplates.a.cloneNode(false), + text = elementTemplates.span.cloneNode(false), + header = this.options.header && this.$menu.find('.' + classNames.POPOVERHEADER).length > 0 ? this.$menu.find('.' + classNames.POPOVERHEADER)[0].cloneNode(true) : null, + search = this.options.liveSearch ? elementTemplates.div.cloneNode(false) : null, + actions = this.options.actionsBox && this.multiple && this.$menu.find('.bs-actionsbox').length > 0 ? this.$menu.find('.bs-actionsbox')[0].cloneNode(true) : null, + doneButton = this.options.doneButton && this.multiple && this.$menu.find('.bs-donebutton').length > 0 ? this.$menu.find('.bs-donebutton')[0].cloneNode(true) : null, + firstOption = this.$element[0].options[0]; + + this.sizeInfo.selectWidth = this.$newElement[0].offsetWidth; + + text.className = 'text'; + a.className = 'dropdown-item ' + (firstOption ? firstOption.className : ''); + newElement.className = this.$menu[0].parentNode.className + ' ' + classNames.SHOW; + newElement.style.width = 0; // ensure button width doesn't affect natural width of menu when calculating + if (this.options.width === 'auto') menu.style.minWidth = 0; + menu.className = classNames.MENU + ' ' + classNames.SHOW; + menuInner.className = 'inner ' + classNames.SHOW; + menuInnerInner.className = classNames.MENU + ' inner ' + (version.major >= '4' ? classNames.SHOW : ''); + divider.className = classNames.DIVIDER; + dropdownHeader.className = 'dropdown-header'; + + text.appendChild(document.createTextNode('\u200b')); + + if (this.selectpicker.current.data.length) { + for (var i = 0; i < this.selectpicker.current.data.length; i++) { + var data = this.selectpicker.current.data[i]; + if (data.type === 'option' && $(data.element.firstChild).css('display') !== 'none') { + li = data.element; + break; + } + } + } else { + li = elementTemplates.li.cloneNode(false); + a.appendChild(text); + li.appendChild(a); + } + + dropdownHeader.appendChild(text.cloneNode(true)); + + if (this.selectpicker.view.widestOption) { + menuInnerInner.appendChild(this.selectpicker.view.widestOption.cloneNode(true)); + } + + menuInnerInner.appendChild(li); + menuInnerInner.appendChild(divider); + menuInnerInner.appendChild(dropdownHeader); + if (header) menu.appendChild(header); + if (search) { + var input = document.createElement('input'); + search.className = 'bs-searchbox'; + input.className = 'form-control'; + search.appendChild(input); + menu.appendChild(search); + } + if (actions) menu.appendChild(actions); + menuInner.appendChild(menuInnerInner); + menu.appendChild(menuInner); + if (doneButton) menu.appendChild(doneButton); + newElement.appendChild(menu); + + document.body.appendChild(newElement); + + var liHeight = li.offsetHeight, + dropdownHeaderHeight = dropdownHeader ? dropdownHeader.offsetHeight : 0, + headerHeight = header ? header.offsetHeight : 0, + searchHeight = search ? search.offsetHeight : 0, + actionsHeight = actions ? actions.offsetHeight : 0, + doneButtonHeight = doneButton ? doneButton.offsetHeight : 0, + dividerHeight = $(divider).outerHeight(true), + menuStyle = window.getComputedStyle(menu), + menuWidth = menu.offsetWidth, + menuPadding = { + vert: toInteger(menuStyle.paddingTop) + + toInteger(menuStyle.paddingBottom) + + toInteger(menuStyle.borderTopWidth) + + toInteger(menuStyle.borderBottomWidth), + horiz: toInteger(menuStyle.paddingLeft) + + toInteger(menuStyle.paddingRight) + + toInteger(menuStyle.borderLeftWidth) + + toInteger(menuStyle.borderRightWidth) + }, + menuExtras = { + vert: menuPadding.vert + + toInteger(menuStyle.marginTop) + + toInteger(menuStyle.marginBottom) + 2, + horiz: menuPadding.horiz + + toInteger(menuStyle.marginLeft) + + toInteger(menuStyle.marginRight) + 2 + }, + scrollBarWidth; + + menuInner.style.overflowY = 'scroll'; + + scrollBarWidth = menu.offsetWidth - menuWidth; + + document.body.removeChild(newElement); + + this.sizeInfo.liHeight = liHeight; + this.sizeInfo.dropdownHeaderHeight = dropdownHeaderHeight; + this.sizeInfo.headerHeight = headerHeight; + this.sizeInfo.searchHeight = searchHeight; + this.sizeInfo.actionsHeight = actionsHeight; + this.sizeInfo.doneButtonHeight = doneButtonHeight; + this.sizeInfo.dividerHeight = dividerHeight; + this.sizeInfo.menuPadding = menuPadding; + this.sizeInfo.menuExtras = menuExtras; + this.sizeInfo.menuWidth = menuWidth; + this.sizeInfo.menuInnerInnerWidth = menuWidth - menuPadding.horiz; + this.sizeInfo.totalMenuWidth = this.sizeInfo.menuWidth; + this.sizeInfo.scrollBarWidth = scrollBarWidth; + this.sizeInfo.selectHeight = this.$newElement[0].offsetHeight; + + this.setPositionData(); + }, + + getSelectPosition: function () { + var that = this, + $window = $(window), + pos = that.$newElement.offset(), + $container = $(that.options.container), + containerPos; + + if (that.options.container && $container.length && !$container.is('body')) { + containerPos = $container.offset(); + containerPos.top += parseInt($container.css('borderTopWidth')); + containerPos.left += parseInt($container.css('borderLeftWidth')); + } else { + containerPos = { top: 0, left: 0 }; + } + + var winPad = that.options.windowPadding; + + this.sizeInfo.selectOffsetTop = pos.top - containerPos.top - $window.scrollTop(); + this.sizeInfo.selectOffsetBot = $window.height() - this.sizeInfo.selectOffsetTop - this.sizeInfo.selectHeight - containerPos.top - winPad[2]; + this.sizeInfo.selectOffsetLeft = pos.left - containerPos.left - $window.scrollLeft(); + this.sizeInfo.selectOffsetRight = $window.width() - this.sizeInfo.selectOffsetLeft - this.sizeInfo.selectWidth - containerPos.left - winPad[1]; + this.sizeInfo.selectOffsetTop -= winPad[0]; + this.sizeInfo.selectOffsetLeft -= winPad[3]; + }, + + setMenuSize: function (isAuto) { + this.getSelectPosition(); + + var selectWidth = this.sizeInfo.selectWidth, + liHeight = this.sizeInfo.liHeight, + headerHeight = this.sizeInfo.headerHeight, + searchHeight = this.sizeInfo.searchHeight, + actionsHeight = this.sizeInfo.actionsHeight, + doneButtonHeight = this.sizeInfo.doneButtonHeight, + divHeight = this.sizeInfo.dividerHeight, + menuPadding = this.sizeInfo.menuPadding, + menuInnerHeight, + menuHeight, + divLength = 0, + minHeight, + _minHeight, + maxHeight, + menuInnerMinHeight, + estimate, + isDropup; + + if (this.options.dropupAuto) { + // Get the estimated height of the menu without scrollbars. + // This is useful for smaller menus, where there might be plenty of room + // below the button without setting dropup, but we can't know + // the exact height of the menu until createView is called later + estimate = liHeight * this.selectpicker.current.data.length + menuPadding.vert; + + isDropup = this.sizeInfo.selectOffsetTop - this.sizeInfo.selectOffsetBot > this.sizeInfo.menuExtras.vert && estimate + this.sizeInfo.menuExtras.vert + 50 > this.sizeInfo.selectOffsetBot; + + // ensure dropup doesn't change while searching (so menu doesn't bounce back and forth) + if (this.selectpicker.isSearching === true) { + isDropup = this.selectpicker.dropup; + } + + this.$newElement.toggleClass(classNames.DROPUP, isDropup); + this.selectpicker.dropup = isDropup; + } + + if (this.options.size === 'auto') { + _minHeight = this.selectpicker.current.data.length > 3 ? this.sizeInfo.liHeight * 3 + this.sizeInfo.menuExtras.vert - 2 : 0; + menuHeight = this.sizeInfo.selectOffsetBot - this.sizeInfo.menuExtras.vert; + minHeight = _minHeight + headerHeight + searchHeight + actionsHeight + doneButtonHeight; + menuInnerMinHeight = Math.max(_minHeight - menuPadding.vert, 0); + + if (this.$newElement.hasClass(classNames.DROPUP)) { + menuHeight = this.sizeInfo.selectOffsetTop - this.sizeInfo.menuExtras.vert; + } + + maxHeight = menuHeight; + menuInnerHeight = menuHeight - headerHeight - searchHeight - actionsHeight - doneButtonHeight - menuPadding.vert; + } else if (this.options.size && this.options.size != 'auto' && this.selectpicker.current.elements.length > this.options.size) { + for (var i = 0; i < this.options.size; i++) { + if (this.selectpicker.current.data[i].type === 'divider') divLength++; + } + + menuHeight = liHeight * this.options.size + divLength * divHeight + menuPadding.vert; + menuInnerHeight = menuHeight - menuPadding.vert; + maxHeight = menuHeight + headerHeight + searchHeight + actionsHeight + doneButtonHeight; + minHeight = menuInnerMinHeight = ''; + } + + this.$menu.css({ + 'max-height': maxHeight + 'px', + 'overflow': 'hidden', + 'min-height': minHeight + 'px' + }); + + this.$menuInner.css({ + 'max-height': menuInnerHeight + 'px', + 'overflow': 'hidden auto', + 'min-height': menuInnerMinHeight + 'px' + }); + + // ensure menuInnerHeight is always a positive number to prevent issues calculating chunkSize in createView + this.sizeInfo.menuInnerHeight = Math.max(menuInnerHeight, 1); + + if (this.selectpicker.current.data.length && this.selectpicker.current.data[this.selectpicker.current.data.length - 1].position > this.sizeInfo.menuInnerHeight) { + this.sizeInfo.hasScrollBar = true; + this.sizeInfo.totalMenuWidth = this.sizeInfo.menuWidth + this.sizeInfo.scrollBarWidth; + } + + if (this.options.dropdownAlignRight === 'auto') { + this.$menu.toggleClass(classNames.MENURIGHT, this.sizeInfo.selectOffsetLeft > this.sizeInfo.selectOffsetRight && this.sizeInfo.selectOffsetRight < (this.sizeInfo.totalMenuWidth - selectWidth)); + } + + if (this.dropdown && this.dropdown._popper) this.dropdown._popper.update(); + }, + + setSize: function (refresh) { + this.liHeight(refresh); + + if (this.options.header) this.$menu.css('padding-top', 0); + + if (this.options.size !== false) { + var that = this, + $window = $(window); + + this.setMenuSize(); + + if (this.options.liveSearch) { + this.$searchbox + .off('input.setMenuSize propertychange.setMenuSize') + .on('input.setMenuSize propertychange.setMenuSize', function () { + return that.setMenuSize(); + }); + } + + if (this.options.size === 'auto') { + $window + .off('resize' + EVENT_KEY + '.' + this.selectId + '.setMenuSize' + ' scroll' + EVENT_KEY + '.' + this.selectId + '.setMenuSize') + .on('resize' + EVENT_KEY + '.' + this.selectId + '.setMenuSize' + ' scroll' + EVENT_KEY + '.' + this.selectId + '.setMenuSize', function () { + return that.setMenuSize(); + }); + } else if (this.options.size && this.options.size != 'auto' && this.selectpicker.current.elements.length > this.options.size) { + $window.off('resize' + EVENT_KEY + '.' + this.selectId + '.setMenuSize' + ' scroll' + EVENT_KEY + '.' + this.selectId + '.setMenuSize'); + } + } + + this.createView(false, true, refresh); + }, + + setWidth: function () { + var that = this; + + if (this.options.width === 'auto') { + requestAnimationFrame(function () { + that.$menu.css('min-width', '0'); + + that.$element.on('loaded' + EVENT_KEY, function () { + that.liHeight(); + that.setMenuSize(); + + // Get correct width if element is hidden + var $selectClone = that.$newElement.clone().appendTo('body'), + btnWidth = $selectClone.css('width', 'auto').children('button').outerWidth(); + + $selectClone.remove(); + + // Set width to whatever's larger, button title or longest option + that.sizeInfo.selectWidth = Math.max(that.sizeInfo.totalMenuWidth, btnWidth); + that.$newElement.css('width', that.sizeInfo.selectWidth + 'px'); + }); + }); + } else if (this.options.width === 'fit') { + // Remove inline min-width so width can be changed from 'auto' + this.$menu.css('min-width', ''); + this.$newElement.css('width', '').addClass('fit-width'); + } else if (this.options.width) { + // Remove inline min-width so width can be changed from 'auto' + this.$menu.css('min-width', ''); + this.$newElement.css('width', this.options.width); + } else { + // Remove inline min-width/width so width can be changed + this.$menu.css('min-width', ''); + this.$newElement.css('width', ''); + } + // Remove fit-width class if width is changed programmatically + if (this.$newElement.hasClass('fit-width') && this.options.width !== 'fit') { + this.$newElement[0].classList.remove('fit-width'); + } + }, + + selectPosition: function () { + this.$bsContainer = $('
    '); + + var that = this, + $container = $(this.options.container), + pos, + containerPos, + actualHeight, + getPlacement = function ($element) { + var containerPosition = {}, + // fall back to dropdown's default display setting if display is not manually set + display = that.options.display || ( + // Bootstrap 3 doesn't have $.fn.dropdown.Constructor.Default + $.fn.dropdown.Constructor.Default ? $.fn.dropdown.Constructor.Default.display + : false + ); + + that.$bsContainer.addClass($element.attr('class').replace(/form-control|fit-width/gi, '')).toggleClass(classNames.DROPUP, $element.hasClass(classNames.DROPUP)); + pos = $element.offset(); + + if (!$container.is('body')) { + containerPos = $container.offset(); + containerPos.top += parseInt($container.css('borderTopWidth')) - $container.scrollTop(); + containerPos.left += parseInt($container.css('borderLeftWidth')) - $container.scrollLeft(); + } else { + containerPos = { top: 0, left: 0 }; + } + + actualHeight = $element.hasClass(classNames.DROPUP) ? 0 : $element[0].offsetHeight; + + // Bootstrap 4+ uses Popper for menu positioning + if (version.major < 4 || display === 'static') { + containerPosition.top = pos.top - containerPos.top + actualHeight; + containerPosition.left = pos.left - containerPos.left; + } + + containerPosition.width = $element[0].offsetWidth; + + that.$bsContainer.css(containerPosition); + }; + + this.$button.on('click.bs.dropdown.data-api', function () { + if (that.isDisabled()) { + return; + } + + getPlacement(that.$newElement); + + that.$bsContainer + .appendTo(that.options.container) + .toggleClass(classNames.SHOW, !that.$button.hasClass(classNames.SHOW)) + .append(that.$menu); + }); + + $(window) + .off('resize' + EVENT_KEY + '.' + this.selectId + ' scroll' + EVENT_KEY + '.' + this.selectId) + .on('resize' + EVENT_KEY + '.' + this.selectId + ' scroll' + EVENT_KEY + '.' + this.selectId, function () { + var isActive = that.$newElement.hasClass(classNames.SHOW); + + if (isActive) getPlacement(that.$newElement); + }); + + this.$element.on('hide' + EVENT_KEY, function () { + that.$menu.data('height', that.$menu.height()); + that.$bsContainer.detach(); + }); + }, + + createOption: function (data, init) { + var optionData = !data.option ? data : data.option; + + if (optionData && optionData.nodeType !== 1) { + var option = (init ? elementTemplates.selectedOption : elementTemplates.option).cloneNode(true); + if (optionData.value !== undefined) option.value = optionData.value; + option.textContent = optionData.text; + + option.selected = true; + + if (optionData.liIndex !== undefined) { + option.liIndex = optionData.liIndex; + } else if (!init) { + option.liIndex = data.index; + } + + data.option = option; + + this.selectpicker.main.optionQueue.appendChild(option); + } + }, + + setOptionStatus: function (selectedOnly) { + var that = this; + + that.noScroll = false; + + if (that.selectpicker.view.visibleElements && that.selectpicker.view.visibleElements.length) { + for (var i = 0; i < that.selectpicker.view.visibleElements.length; i++) { + var liData = that.selectpicker.current.data[i + that.selectpicker.view.position0], + option = liData.option; + + if (option) { + if (selectedOnly !== true) { + that.setDisabled(liData); + } + + that.setSelected(liData); + } + } + + // append optionQueue (documentFragment with option elements for select options) + if (this.options.source.data) this.$element[0].appendChild(this.selectpicker.main.optionQueue); + } + }, + + /** + * @param {Object} liData - the option object that is being changed + * @param {boolean} selected - true if the option is being selected, false if being deselected + */ + setSelected: function (liData, selected) { + selected = selected === undefined ? liData.selected : selected; + + var li = liData.element, + activeElementIsSet = this.activeElement !== undefined, + thisIsActive = this.activeElement === li, + prevActive, + a, + // if current option is already active + // OR + // if the current option is being selected, it's NOT multiple, and + // activeElement is undefined: + // - when the menu is first being opened, OR + // - after a search has been performed, OR + // - when retainActive is false when selecting a new option (i.e. index of the newly selected option is not the same as the current activeElement) + keepActive = thisIsActive || (selected && !this.multiple && !activeElementIsSet); + + if (!li) return; + + if (selected !== undefined) { + liData.selected = selected; + if (liData.option) liData.option.selected = selected; + } + + if (selected && this.options.source.data) { + this.createOption(liData, false); + } + + a = li.firstChild; + + if (selected) { + this.selectedElement = li; + } + + li.classList.toggle('selected', selected); + + if (keepActive) { + this.focusItem(li, liData); + this.selectpicker.view.currentActive = li; + this.activeElement = li; + } else { + this.defocusItem(li); + } + + if (a) { + a.classList.toggle('selected', selected); + + if (selected) { + a.setAttribute('aria-selected', true); + } else { + if (this.multiple) { + a.setAttribute('aria-selected', false); + } else { + a.removeAttribute('aria-selected'); + } + } + } + + if (!keepActive && !activeElementIsSet && selected && this.prevActiveElement !== undefined) { + prevActive = this.prevActiveElement; + + this.defocusItem(prevActive); + } + }, + + /** + * @param {number} index - the index of the option that is being disabled + * @param {boolean} disabled - true if the option is being disabled, false if being enabled + */ + setDisabled: function (liData) { + var disabled = liData.disabled, + li = liData.element, + a; + + if (!li) return; + + a = li.firstChild; + + li.classList.toggle(classNames.DISABLED, disabled); + + if (a) { + if (version.major >= '4') a.classList.toggle(classNames.DISABLED, disabled); + + if (disabled) { + a.setAttribute('aria-disabled', disabled); + a.setAttribute('tabindex', -1); + } else { + a.removeAttribute('aria-disabled'); + a.setAttribute('tabindex', 0); + } + } + }, + + isDisabled: function () { + return this.$element[0].disabled; + }, + + checkDisabled: function () { + if (this.isDisabled()) { + this.$newElement[0].classList.add(classNames.DISABLED); + this.$button.addClass(classNames.DISABLED).attr('aria-disabled', true); + } else { + if (this.$button[0].classList.contains(classNames.DISABLED)) { + this.$newElement[0].classList.remove(classNames.DISABLED); + this.$button.removeClass(classNames.DISABLED).attr('aria-disabled', false); + } + } + }, + + clickListener: function () { + var that = this, + $document = $(document); + + $document.data('spaceSelect', false); + + this.$button.on('keyup', function (e) { + if (/(32)/.test(e.keyCode.toString(10)) && $document.data('spaceSelect')) { + e.preventDefault(); + $document.data('spaceSelect', false); + } + }); + + this.$newElement.on('show.bs.dropdown', function () { + if (!that.dropdown && version.major === '4') { + that.dropdown = that.$button.data('bs.dropdown'); + that.dropdown._menu = that.$menu[0]; + } + }); + + function clearSelection (e) { + if (that.multiple) { + that.deselectAll(); + } else { + var element = that.$element[0], + prevValue = element.value, + prevIndex = element.selectedIndex, + prevOption = element.options[prevIndex], + prevData = prevOption ? that.selectpicker.main.data[prevOption.liIndex] : false; + + if (prevData) { + that.setSelected(prevData, false); + } + + element.selectedIndex = 0; + + changedArguments = [prevIndex, false, prevValue]; + that.$element.triggerNative('change'); + } + + // remove selected styling if menu is open + if (that.$newElement.hasClass(classNames.SHOW)) { + if (that.options.liveSearch) { + that.$searchbox.trigger('focus'); + } + + that.createView(false); + } + } + + this.$button.on('click.bs.dropdown.data-api', function (e) { + if (that.options.allowClear) { + var target = e.target, + clearButton = that.$clearButton[0]; + + // IE doesn't support event listeners on child elements of buttons + if (/MSIE|Trident/.test(window.navigator.userAgent)) { + target = document.elementFromPoint(e.clientX, e.clientY); + } + + if (target === clearButton || target.parentElement === clearButton) { + e.stopImmediatePropagation(); + clearSelection(e); + } + } + + if (!that.$newElement.hasClass(classNames.SHOW)) { + that.setSize(); + } + }); + + function setFocus () { + if (that.options.liveSearch) { + that.$searchbox.trigger('focus'); + } else { + that.$menuInner.trigger('focus'); + } + } + + function checkPopperExists () { + if (that.dropdown && that.dropdown._popper && that.dropdown._popper.state) { + setFocus(); + } else { + requestAnimationFrame(checkPopperExists); + } + } + + this.$element.on('shown' + EVENT_KEY, function () { + if (that.$menuInner[0].scrollTop !== that.selectpicker.view.scrollTop) { + that.$menuInner[0].scrollTop = that.selectpicker.view.scrollTop; + } + + if (version.major > 3) { + requestAnimationFrame(checkPopperExists); + } else { + setFocus(); + } + }); + + // ensure posinset and setsize are correct before selecting an option via a click + this.$menuInner.on('mouseenter', 'li a', function (e) { + var hoverLi = this.parentElement, + position0 = that.isVirtual() ? that.selectpicker.view.position0 : 0, + index = Array.prototype.indexOf.call(hoverLi.parentElement.children, hoverLi), + hoverData = that.selectpicker.current.data[index + position0]; + + that.focusItem(hoverLi, hoverData, true); + }); + + this.$menuInner.on('click', 'li a', function (e, retainActive) { + var $this = $(this), + element = that.$element[0], + position0 = that.isVirtual() ? that.selectpicker.view.position0 : 0, + clickedData = that.selectpicker.current.data[$this.parent().index() + position0], + clickedElement = clickedData.element, + prevValue = getSelectValues.call(that), + prevIndex = element.selectedIndex, + prevOption = element.options[prevIndex], + prevData = prevOption ? that.selectpicker.main.data[prevOption.liIndex] : false, + triggerChange = true; + + // Don't close on multi choice menu + if (that.multiple && that.options.maxOptions !== 1) { + e.stopPropagation(); + } + + e.preventDefault(); + + // Don't run if the select is disabled + if (!that.isDisabled() && !$this.parent().hasClass(classNames.DISABLED)) { + var option = clickedData.option, + $option = $(option), + state = option.selected, + optgroupData = that.selectpicker.current.data.find(function (datum) { + return datum.optID === clickedData.optID && datum.type === 'optgroup-label'; + }), + optgroup = optgroupData ? optgroupData.optgroup : undefined, + dataGetter = optgroup instanceof Element ? getOptionData.fromOption : getOptionData.fromDataSource, + optgroupOptions = optgroup && optgroup.children, + maxOptions = parseInt(that.options.maxOptions), + maxOptionsGrp = optgroup && parseInt(dataGetter(optgroup, 'maxOptions')) || false; + + if (clickedElement === that.activeElement) retainActive = true; + + if (!retainActive) { + that.prevActiveElement = that.activeElement; + that.activeElement = undefined; + } + + if (!that.multiple || maxOptions === 1) { // Deselect previous option if not multi select + if (prevData) that.setSelected(prevData, false); + that.setSelected(clickedData, true); + } else { // Toggle the clicked option if multi select. + that.setSelected(clickedData, !state); + that.focusedParent.focus(); + + if (maxOptions !== false || maxOptionsGrp !== false) { + var maxReached = maxOptions < getSelectedOptions.call(that).length, + selectedGroupOptions = 0; + + if (optgroup && optgroup.children) { + for (var i = 0; i < optgroup.children.length; i++) { + if (optgroup.children[i].selected) selectedGroupOptions++; + } + } + + var maxReachedGrp = maxOptionsGrp < selectedGroupOptions; + + if ((maxOptions && maxReached) || (maxOptionsGrp && maxReachedGrp)) { + if (maxOptions && maxOptions === 1) { + element.selectedIndex = -1; + that.setOptionStatus(true); + } else if (maxOptionsGrp && maxOptionsGrp === 1) { + for (var i = 0; i < optgroupOptions.length; i++) { + var _option = optgroupOptions[i]; + that.setSelected(that.selectpicker.current.data[_option.liIndex], false); + } + + that.setSelected(clickedData, true); + } else { + var maxOptionsText = typeof that.options.maxOptionsText === 'string' ? [that.options.maxOptionsText, that.options.maxOptionsText] : that.options.maxOptionsText, + maxOptionsArr = typeof maxOptionsText === 'function' ? maxOptionsText(maxOptions, maxOptionsGrp) : maxOptionsText, + maxTxt = maxOptionsArr[0].replace('{n}', maxOptions), + maxTxtGrp = maxOptionsArr[1].replace('{n}', maxOptionsGrp), + $notify = $('
    '); + // If {var} is set in array, replace it + /** @deprecated */ + if (maxOptionsArr[2]) { + maxTxt = maxTxt.replace('{var}', maxOptionsArr[2][maxOptions > 1 ? 0 : 1]); + maxTxtGrp = maxTxtGrp.replace('{var}', maxOptionsArr[2][maxOptionsGrp > 1 ? 0 : 1]); + } + + that.$menu.append($notify); + + if (maxOptions && maxReached) { + $notify.append($('
    ' + maxTxt + '
    ')); + triggerChange = false; + that.$element.trigger('maxReached' + EVENT_KEY); + } + + if (maxOptionsGrp && maxReachedGrp) { + $notify.append($('
    ' + maxTxtGrp + '
    ')); + triggerChange = false; + that.$element.trigger('maxReachedGrp' + EVENT_KEY); + } + + setTimeout(function () { + that.setSelected(clickedData, false); + }, 10); + + $notify[0].classList.add('fadeOut'); + + setTimeout(function () { + $notify.remove(); + }, 1050); + } + } + } + } + + if (that.options.source.data) that.$element[0].appendChild(that.selectpicker.main.optionQueue); + + if (!that.multiple || (that.multiple && that.options.maxOptions === 1)) { + that.$button.trigger('focus'); + } else if (that.options.liveSearch) { + that.$searchbox.trigger('focus'); + } + + // Trigger select 'change' + if (triggerChange) { + if (that.multiple || prevIndex !== element.selectedIndex) { + // $option.prop('selected') is current option state (selected/unselected). prevValue is the value of the select prior to being changed. + changedArguments = [option.index, $option.prop('selected'), prevValue]; + that.$element + .triggerNative('change'); + } + } + } + }); + + this.$menu.on('click', 'li.' + classNames.DISABLED + ' a, .' + classNames.POPOVERHEADER + ', .' + classNames.POPOVERHEADER + ' :not(.close)', function (e) { + if (e.currentTarget == this) { + e.preventDefault(); + e.stopPropagation(); + if (that.options.liveSearch && !$(e.target).hasClass('close')) { + that.$searchbox.trigger('focus'); + } else { + that.$button.trigger('focus'); + } + } + }); + + this.$menuInner.on('click', '.divider, .dropdown-header', function (e) { + e.preventDefault(); + e.stopPropagation(); + if (that.options.liveSearch) { + that.$searchbox.trigger('focus'); + } else { + that.$button.trigger('focus'); + } + }); + + this.$menu.on('click', '.' + classNames.POPOVERHEADER + ' .close', function () { + that.$button.trigger('click'); + }); + + this.$searchbox.on('click', function (e) { + e.stopPropagation(); + }); + + this.$menu.on('click', '.actions-btn', function (e) { + if (that.options.liveSearch) { + that.$searchbox.trigger('focus'); + } else { + that.$button.trigger('focus'); + } + + e.preventDefault(); + e.stopPropagation(); + + if ($(this).hasClass('bs-select-all')) { + that.selectAll(); + } else { + that.deselectAll(); + } + }); + + this.$button + .on('focus' + EVENT_KEY, function (e) { + var tabindex = that.$element[0].getAttribute('tabindex'); + + // only change when button is actually focused + if (tabindex !== undefined && e.originalEvent && e.originalEvent.isTrusted) { + // apply select element's tabindex to ensure correct order is followed when tabbing to the next element + this.setAttribute('tabindex', tabindex); + // set element's tabindex to -1 to allow for reverse tabbing + that.$element[0].setAttribute('tabindex', -1); + that.selectpicker.view.tabindex = tabindex; + } + }) + .on('blur' + EVENT_KEY, function (e) { + // revert everything to original tabindex + if (that.selectpicker.view.tabindex !== undefined && e.originalEvent && e.originalEvent.isTrusted) { + that.$element[0].setAttribute('tabindex', that.selectpicker.view.tabindex); + this.setAttribute('tabindex', -1); + that.selectpicker.view.tabindex = undefined; + } + }); + + this.$element + .on('change' + EVENT_KEY, function () { + that.render(); + that.$element.trigger('changed' + EVENT_KEY, changedArguments); + changedArguments = null; + }) + .on('focus' + EVENT_KEY, function () { + if (!that.options.mobile) that.$button[0].focus(); + }); + }, + + liveSearchListener: function () { + var that = this; + + this.$button.on('click.bs.dropdown.data-api', function () { + if (!!that.$searchbox.val()) { + that.$searchbox.val(''); + that.selectpicker.search.previousValue = undefined; + } + }); + + this.$searchbox.on('click.bs.dropdown.data-api focus.bs.dropdown.data-api touchend.bs.dropdown.data-api', function (e) { + e.stopPropagation(); + }); + + this.$searchbox.on('input propertychange', function () { + var searchValue = that.$searchbox[0].value; + + that.selectpicker.search.elements = []; + that.selectpicker.search.data = []; + + if (searchValue) { + that.selectpicker.search.previousValue = searchValue; + + if (that.options.source.search) { + that.fetchData(function (builtData) { + that.render(); + that.buildList(undefined, true); + that.noScroll = true; + that.$menuInner.scrollTop(0); + that.createView(true); + showNoResults.call(that, builtData, searchValue); + }, 'search', 0, searchValue); + } else { + var i, + searchMatch = [], + q = searchValue.toUpperCase(), + cache = {}, + cacheArr = [], + searchStyle = that._searchStyle(), + normalizeSearch = that.options.liveSearchNormalize; + + if (normalizeSearch) q = normalizeToBase(q); + + for (var i = 0; i < that.selectpicker.main.data.length; i++) { + var li = that.selectpicker.main.data[i]; + + if (!cache[i]) { + cache[i] = stringSearch(li, q, searchStyle, normalizeSearch); + } + + if (cache[i] && li.headerIndex !== undefined && cacheArr.indexOf(li.headerIndex) === -1) { + if (li.headerIndex > 0) { + cache[li.headerIndex - 1] = true; + cacheArr.push(li.headerIndex - 1); + } + + cache[li.headerIndex] = true; + cacheArr.push(li.headerIndex); + + cache[li.lastIndex + 1] = true; + } + + if (cache[i] && li.type !== 'optgroup-label') cacheArr.push(i); + } + + for (var i = 0, cacheLen = cacheArr.length; i < cacheLen; i++) { + var index = cacheArr[i], + prevIndex = cacheArr[i - 1], + li = that.selectpicker.main.data[index], + liPrev = that.selectpicker.main.data[prevIndex]; + + if (li.type !== 'divider' || (li.type === 'divider' && liPrev && liPrev.type !== 'divider' && cacheLen - 1 !== i)) { + that.selectpicker.search.data.push(li); + searchMatch.push(that.selectpicker.main.elements[index]); + } + } + + that.activeElement = undefined; + that.noScroll = true; + that.$menuInner.scrollTop(0); + that.selectpicker.search.elements = searchMatch; + that.createView(true); + showNoResults.call(that, searchMatch, searchValue); + } + } else if (that.selectpicker.search.previousValue) { // for IE11 (#2402) + that.$menuInner.scrollTop(0); + that.createView(false); + } + }); + }, + + _searchStyle: function () { + return this.options.liveSearchStyle || 'contains'; + }, + + val: function (value) { + var element = this.$element[0]; + + if (typeof value !== 'undefined') { + var selectedOptions = getSelectedOptions.call(this), + prevValue = getSelectValues.call(this, selectedOptions); + + changedArguments = [null, null, prevValue]; + + if (!Array.isArray(value)) value = [ value ]; + + value.map(String); + + for (var i = 0; i < selectedOptions.length; i++) { + var item = selectedOptions[i]; + + if (item && value.indexOf(String(item.value)) === -1) { + this.setSelected(item, false); + } + } + + // only update selected value if it matches an existing option + this.selectpicker.main.data.filter(function (item) { + if (value.indexOf(String(item.value)) !== -1) { + this.setSelected(item, true); + return true; + } + + return false; + }, this); + + if (this.options.source.data) element.appendChild(this.selectpicker.main.optionQueue); + + this.$element.trigger('changed' + EVENT_KEY, changedArguments); + + if (this.$newElement.hasClass(classNames.SHOW)) { + if (this.multiple) { + this.setOptionStatus(true); + } else { + var liSelectedIndex = (element.options[element.selectedIndex] || {}).liIndex; + + if (typeof liSelectedIndex === 'number') { + this.setSelected(this.selectpicker.current.data[liSelectedIndex], true); + } + } + } + + this.render(); + + changedArguments = null; + + return this.$element; + } else { + return this.$element.val(); + } + }, + + changeAll: function (status) { + if (!this.multiple) return; + if (typeof status === 'undefined') status = true; + + var element = this.$element[0], + previousSelected = 0, + currentSelected = 0, + prevValue = getSelectValues.call(this); + + element.classList.add('bs-select-hidden'); + + for (var i = 0, data = this.selectpicker.current.data, len = data.length; i < len; i++) { + var liData = data[i], + option = liData.option; + + if (option && !liData.disabled && liData.type !== 'divider') { + if (liData.selected) previousSelected++; + option.selected = status; + liData.selected = status; + if (status === true) currentSelected++; + } + } + + element.classList.remove('bs-select-hidden'); + + if (previousSelected === currentSelected) return; + + this.setOptionStatus(); + + changedArguments = [null, null, prevValue]; + + this.$element + .triggerNative('change'); + }, + + selectAll: function () { + return this.changeAll(true); + }, + + deselectAll: function () { + return this.changeAll(false); + }, + + toggle: function (e, state) { + var isActive, + triggerClick = state === undefined; + + e = e || window.event; + + if (e) e.stopPropagation(); + + if (triggerClick === false) { + isActive = this.$newElement[0].classList.contains(classNames.SHOW); + triggerClick = state === true && isActive === false || state === false && isActive === true; + } + + if (triggerClick) this.$button.trigger('click.bs.dropdown.data-api'); + }, + + open: function (e) { + this.toggle(e, true); + }, + + close: function (e) { + this.toggle(e, false); + }, + + keydown: function (e) { + var $this = $(this), + isToggle = $this.hasClass('dropdown-toggle'), + $parent = isToggle ? $this.closest('.dropdown') : $this.closest(Selector.MENU), + that = $parent.data('this'), + $items = that.findLis(), + index, + isActive, + liActive, + activeLi, + offset, + updateScroll = false, + downOnTab = e.which === keyCodes.TAB && !isToggle && !that.options.selectOnTab, + isArrowKey = REGEXP_ARROW.test(e.which) || downOnTab, + scrollTop = that.$menuInner[0].scrollTop, + isVirtual = that.isVirtual(), + position0 = isVirtual === true ? that.selectpicker.view.position0 : 0; + + // do nothing if a function key is pressed + if (e.which >= 112 && e.which <= 123) return; + + isActive = that.$menu.hasClass(classNames.SHOW); + + if ( + !isActive && + ( + isArrowKey || + (e.which >= 48 && e.which <= 57) || + (e.which >= 96 && e.which <= 105) || + (e.which >= 65 && e.which <= 90) + ) + ) { + that.$button.trigger('click.bs.dropdown.data-api'); + + if (that.options.liveSearch) { + that.$searchbox.trigger('focus'); + return; + } + } + + if (e.which === keyCodes.ESCAPE && isActive) { + e.preventDefault(); + that.$button.trigger('click.bs.dropdown.data-api').trigger('focus'); + } + + if (isArrowKey) { // if up or down + if (!$items.length) return; + + liActive = that.activeElement; + index = liActive ? Array.prototype.indexOf.call(liActive.parentElement.children, liActive) : -1; + + if (index !== -1) { + that.defocusItem(liActive); + } + + if (e.which === keyCodes.ARROW_UP) { // up + if (index !== -1) index--; + if (index + position0 < 0) index += $items.length; + + if (!that.selectpicker.view.canHighlight[index + position0]) { + index = that.selectpicker.view.canHighlight.slice(0, index + position0).lastIndexOf(true) - position0; + if (index === -1) index = $items.length - 1; + } + } else if (e.which === keyCodes.ARROW_DOWN || downOnTab) { // down + index++; + if (index + position0 >= that.selectpicker.view.canHighlight.length) index = that.selectpicker.view.firstHighlightIndex; + + if (!that.selectpicker.view.canHighlight[index + position0]) { + index = index + 1 + that.selectpicker.view.canHighlight.slice(index + position0 + 1).indexOf(true); + } + } + + e.preventDefault(); + + var liActiveIndex = position0 + index; + + if (e.which === keyCodes.ARROW_UP) { // up + // scroll to bottom and highlight last option + if (position0 === 0 && index === $items.length - 1) { + that.$menuInner[0].scrollTop = that.$menuInner[0].scrollHeight; + + liActiveIndex = that.selectpicker.current.elements.length - 1; + } else { + activeLi = that.selectpicker.current.data[liActiveIndex]; + + // could be undefined if no results exist + if (activeLi) { + offset = activeLi.position - activeLi.height; + + updateScroll = offset < scrollTop; + } + } + } else if (e.which === keyCodes.ARROW_DOWN || downOnTab) { // down + // scroll to top and highlight first option + if (index === that.selectpicker.view.firstHighlightIndex) { + that.$menuInner[0].scrollTop = 0; + + liActiveIndex = that.selectpicker.view.firstHighlightIndex; + } else { + activeLi = that.selectpicker.current.data[liActiveIndex]; + + // could be undefined if no results exist + if (activeLi) { + offset = activeLi.position - that.sizeInfo.menuInnerHeight; + + updateScroll = offset > scrollTop; + } + } + } + + liActive = that.selectpicker.current.elements[liActiveIndex]; + + that.activeElement = (that.selectpicker.current.data[liActiveIndex] || {}).element; + + that.focusItem(liActive); + + that.selectpicker.view.currentActive = liActive; + + if (updateScroll) that.$menuInner[0].scrollTop = offset; + + if (that.options.liveSearch) { + that.$searchbox.trigger('focus'); + } else { + $this.trigger('focus'); + } + } else if ( + (!$this.is('input') && !REGEXP_TAB_OR_ESCAPE.test(e.which)) || + (e.which === keyCodes.SPACE && that.selectpicker.keydown.keyHistory) + ) { + var searchMatch, + matches = [], + keyHistory; + + e.preventDefault(); + + that.selectpicker.keydown.keyHistory += keyCodeMap[e.which]; + + if (that.selectpicker.keydown.resetKeyHistory.cancel) clearTimeout(that.selectpicker.keydown.resetKeyHistory.cancel); + that.selectpicker.keydown.resetKeyHistory.cancel = that.selectpicker.keydown.resetKeyHistory.start(); + + keyHistory = that.selectpicker.keydown.keyHistory; + + // if all letters are the same, set keyHistory to just the first character when searching + if (/^(.)\1+$/.test(keyHistory)) { + keyHistory = keyHistory.charAt(0); + } + + // find matches + for (var i = 0; i < that.selectpicker.current.data.length; i++) { + var li = that.selectpicker.current.data[i], + hasMatch; + + hasMatch = stringSearch(li, keyHistory, 'startsWith', true); + + if (hasMatch && that.selectpicker.view.canHighlight[i]) { + matches.push(li.element); + } + } + + if (matches.length) { + var matchIndex = 0; + + $items.removeClass('active').find('a').removeClass('active'); + + // either only one key has been pressed or they are all the same key + if (keyHistory.length === 1) { + matchIndex = matches.indexOf(that.activeElement); + + if (matchIndex === -1 || matchIndex === matches.length - 1) { + matchIndex = 0; + } else { + matchIndex++; + } + } + + searchMatch = matches[matchIndex]; + + activeLi = that.selectpicker.main.data[searchMatch]; + + if (scrollTop - activeLi.position > 0) { + offset = activeLi.position - activeLi.height; + updateScroll = true; + } else { + offset = activeLi.position - that.sizeInfo.menuInnerHeight; + // if the option is already visible at the current scroll position, just keep it the same + updateScroll = activeLi.position > scrollTop + that.sizeInfo.menuInnerHeight; + } + + liActive = that.selectpicker.main.elements[searchMatch]; + + that.activeElement = liActive; + + that.focusItem(liActive); + + if (liActive) liActive.firstChild.focus(); + + if (updateScroll) that.$menuInner[0].scrollTop = offset; + + $this.trigger('focus'); + } + } + + // Select focused option if "Enter", "Spacebar" or "Tab" (when selectOnTab is true) are pressed inside the menu. + if ( + isActive && + ( + (e.which === keyCodes.SPACE && !that.selectpicker.keydown.keyHistory) || + e.which === keyCodes.ENTER || + (e.which === keyCodes.TAB && that.options.selectOnTab) + ) + ) { + if (e.which !== keyCodes.SPACE) e.preventDefault(); + + if (!that.options.liveSearch || e.which !== keyCodes.SPACE) { + that.$menuInner.find('.active a').trigger('click', true); // retain active class + $this.trigger('focus'); + + if (!that.options.liveSearch) { + // Prevent screen from scrolling if the user hits the spacebar + e.preventDefault(); + // Fixes spacebar selection of dropdown items in FF & IE + $(document).data('spaceSelect', true); + } + } + } + }, + + mobile: function () { + // ensure mobile is set to true if mobile function is called after init + this.options.mobile = true; + this.$element[0].classList.add('mobile-device'); + }, + + refresh: function () { + var that = this; + // update options if data attributes have been changed + var config = $.extend({}, this.options, getAttributesObject(this.$element), this.$element.data()); // in this order on refresh, as user may change attributes on select, and options object is not passed on refresh + this.options = config; + + if (this.options.source.data) { + this.render(); + this.buildList(); + } else { + this.fetchData(function () { + that.render(); + that.buildList(); + }); + } + + this.checkDisabled(); + this.setStyle(); + this.setWidth(); + + this.setSize(true); + + this.$element.trigger('refreshed' + EVENT_KEY); + }, + + hide: function () { + this.$newElement.hide(); + }, + + show: function () { + this.$newElement.show(); + }, + + remove: function () { + this.$newElement.remove(); + this.$element.remove(); + }, + + destroy: function () { + this.$newElement.before(this.$element).remove(); + + if (this.$bsContainer) { + this.$bsContainer.remove(); + } else { + this.$menu.remove(); + } + + if (this.selectpicker.view.titleOption && this.selectpicker.view.titleOption.parentNode) { + this.selectpicker.view.titleOption.parentNode.removeChild(this.selectpicker.view.titleOption); + } + + this.$element + .off(EVENT_KEY) + .removeData('selectpicker') + .removeClass('bs-select-hidden selectpicker mobile-device'); + + $(window).off(EVENT_KEY + '.' + this.selectId); + } + }; + + // SELECTPICKER PLUGIN DEFINITION + // ============================== + function Plugin (option) { + // get the args of the outer function.. + var args = arguments; + // The arguments of the function are explicitly re-defined from the argument list, because the shift causes them + // to get lost/corrupted in android 2.3 and IE9 #715 #775 + var _option = option; + + [].shift.apply(args); + + // if the version was not set successfully + if (!version.success) { + // try to retreive it again + try { + version.full = (getVersion() || '').split(' ')[0].split('.'); + } catch (err) { + // fall back to use BootstrapVersion if set + if (Selectpicker.BootstrapVersion) { + version.full = Selectpicker.BootstrapVersion.split(' ')[0].split('.'); + } else { + version.full = [version.major, '0', '0']; + + console.warn( + 'There was an issue retrieving Bootstrap\'s version. ' + + 'Ensure Bootstrap is being loaded before bootstrap-select and there is no namespace collision. ' + + 'If loading Bootstrap asynchronously, the version may need to be manually specified via $.fn.selectpicker.Constructor.BootstrapVersion.', + err + ); + } + } + + version.major = version.full[0]; + version.success = true; + } + + if (version.major >= '4') { + // some defaults need to be changed if using Bootstrap 4 + // check to see if they have already been manually changed before forcing them to update + var toUpdate = []; + + if (Selectpicker.DEFAULTS.style === classNames.BUTTONCLASS) toUpdate.push({ name: 'style', className: 'BUTTONCLASS' }); + if (Selectpicker.DEFAULTS.iconBase === classNames.ICONBASE) toUpdate.push({ name: 'iconBase', className: 'ICONBASE' }); + if (Selectpicker.DEFAULTS.tickIcon === classNames.TICKICON) toUpdate.push({ name: 'tickIcon', className: 'TICKICON' }); + + classNames.DIVIDER = 'dropdown-divider'; + classNames.SHOW = 'show'; + classNames.BUTTONCLASS = 'btn-light'; + classNames.POPOVERHEADER = 'popover-header'; + classNames.ICONBASE = ''; + classNames.TICKICON = 'bs-ok-default'; + + for (var i = 0; i < toUpdate.length; i++) { + var option = toUpdate[i]; + Selectpicker.DEFAULTS[option.name] = classNames[option.className]; + } + } + + if (version.major > '4') { + Selector.DATA_TOGGLE = 'data-bs-toggle="dropdown"'; + } + + var value; + var chain = this.each(function () { + var $this = $(this); + if ($this.is('select')) { + var data = $this.data('selectpicker'), + options = typeof _option == 'object' && _option; + + // for backwards compatibility + // (using title as placeholder is deprecated - remove in v2.0.0) + if (options.title) options.placeholder = options.title; + + if (!data) { + var dataAttributes = $this.data(); + + for (var dataAttr in dataAttributes) { + if (Object.prototype.hasOwnProperty.call(dataAttributes, dataAttr) && $.inArray(dataAttr, DISALLOWED_ATTRIBUTES) !== -1) { + delete dataAttributes[dataAttr]; + } + } + + var config = $.extend({}, Selectpicker.DEFAULTS, $.fn.selectpicker.defaults || {}, getAttributesObject($this), dataAttributes, options); // this is correct order on initial render + config.template = $.extend({}, Selectpicker.DEFAULTS.template, ($.fn.selectpicker.defaults ? $.fn.selectpicker.defaults.template : {}), dataAttributes.template, options.template); + config.source = $.extend({}, Selectpicker.DEFAULTS.source, ($.fn.selectpicker.defaults ? $.fn.selectpicker.defaults.source : {}), options.source); + $this.data('selectpicker', (data = new Selectpicker(this, config))); + } else if (options) { + for (var i in options) { + if (Object.prototype.hasOwnProperty.call(options, i)) { + data.options[i] = options[i]; + } + } + } + + if (typeof _option == 'string') { + if (data[_option] instanceof Function) { + value = data[_option].apply(data, args); + } else { + value = data.options[_option]; + } + } + } + }); + + if (typeof value !== 'undefined') { + // noinspection JSUnusedAssignment + return value; + } else { + return chain; + } + } + + var old = $.fn.selectpicker; + $.fn.selectpicker = Plugin; + $.fn.selectpicker.Constructor = Selectpicker; + + // SELECTPICKER NO CONFLICT + // ======================== + $.fn.selectpicker.noConflict = function () { + $.fn.selectpicker = old; + return this; + }; + + // get Bootstrap's keydown event handler for either Bootstrap 4 or Bootstrap 3 + function keydownHandler () { + if (version.major < 5) { + if ($.fn.dropdown) { + // wait to define until function is called in case Bootstrap isn't loaded yet + var bootstrapKeydown = $.fn.dropdown.Constructor._dataApiKeydownHandler || $.fn.dropdown.Constructor.prototype.keydown; + return bootstrapKeydown.apply(this, arguments); + } + } else { + return Dropdown.dataApiKeydownHandler; + } + } + + $(document) + .off('keydown.bs.dropdown.data-api') + .on('keydown.bs.dropdown.data-api', ':not(.bootstrap-select) > [' + Selector.DATA_TOGGLE + ']', keydownHandler) + .on('keydown.bs.dropdown.data-api', ':not(.bootstrap-select) > .dropdown-menu', keydownHandler) + .on('keydown' + EVENT_KEY, '.bootstrap-select [' + Selector.DATA_TOGGLE + '], .bootstrap-select [role="listbox"], .bootstrap-select .bs-searchbox input', Selectpicker.prototype.keydown) + .on('focusin.modal', '.bootstrap-select [' + Selector.DATA_TOGGLE + '], .bootstrap-select [role="listbox"], .bootstrap-select .bs-searchbox input', function (e) { + e.stopPropagation(); + }); + + // SELECTPICKER DATA-API + // ===================== + document.addEventListener('DOMContentLoaded', function () { + $('.selectpicker').each(function () { + var $selectpicker = $(this); + Plugin.call($selectpicker, $selectpicker.data()); + }); + }); +})(jQuery); diff --git a/mailcow/data/web/js/build/004-datatables.js b/mailcow/data/web/js/build/004-datatables.js new file mode 100644 index 0000000..8c79171 --- /dev/null +++ b/mailcow/data/web/js/build/004-datatables.js @@ -0,0 +1,18781 @@ +/* + * This combined file was created by the DataTables downloader builder: + * https://datatables.net/download + * + * To rebuild or modify this file with the latest versions of the included + * software please visit: + * https://datatables.net/download/#bs5/dt-1.13.1/r-2.4.0/sl-1.5.0 + * + * Included libraries: + * DataTables 1.13.1, Responsive 2.4.0, Select 1.5.0 + */ + +/*! DataTables 1.13.1 + * ©2008-2022 SpryMedia Ltd - datatables.net/license + */ + +/** + * @summary DataTables + * @description Paginate, search and order HTML tables + * @version 1.13.1 + * @author SpryMedia Ltd + * @contact www.datatables.net + * @copyright SpryMedia Ltd. + * + * This source file is free software, available under the following license: + * MIT license - http://datatables.net/license + * + * This source file 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 license files for details. + * + * For details please refer to: http://www.datatables.net + */ + +/*jslint evil: true, undef: true, browser: true */ +/*globals $,require,jQuery,define,_selector_run,_selector_opts,_selector_first,_selector_row_indexes,_ext,_Api,_api_register,_api_registerPlural,_re_new_lines,_re_html,_re_formatted_numeric,_re_escape_regex,_empty,_intVal,_numToDecimal,_isNumber,_isHtml,_htmlNumeric,_pluck,_pluck_order,_range,_stripHtml,_unique,_fnBuildAjax,_fnAjaxUpdate,_fnAjaxParameters,_fnAjaxUpdateDraw,_fnAjaxDataSrc,_fnAddColumn,_fnColumnOptions,_fnAdjustColumnSizing,_fnVisibleToColumnIndex,_fnColumnIndexToVisible,_fnVisbleColumns,_fnGetColumns,_fnColumnTypes,_fnApplyColumnDefs,_fnHungarianMap,_fnCamelToHungarian,_fnLanguageCompat,_fnBrowserDetect,_fnAddData,_fnAddTr,_fnNodeToDataIndex,_fnNodeToColumnIndex,_fnGetCellData,_fnSetCellData,_fnSplitObjNotation,_fnGetObjectDataFn,_fnSetObjectDataFn,_fnGetDataMaster,_fnClearTable,_fnDeleteIndex,_fnInvalidate,_fnGetRowElements,_fnCreateTr,_fnBuildHead,_fnDrawHead,_fnDraw,_fnReDraw,_fnAddOptionsHtml,_fnDetectHeader,_fnGetUniqueThs,_fnFeatureHtmlFilter,_fnFilterComplete,_fnFilterCustom,_fnFilterColumn,_fnFilter,_fnFilterCreateSearch,_fnEscapeRegex,_fnFilterData,_fnFeatureHtmlInfo,_fnUpdateInfo,_fnInfoMacros,_fnInitialise,_fnInitComplete,_fnLengthChange,_fnFeatureHtmlLength,_fnFeatureHtmlPaginate,_fnPageChange,_fnFeatureHtmlProcessing,_fnProcessingDisplay,_fnFeatureHtmlTable,_fnScrollDraw,_fnApplyToChildren,_fnCalculateColumnWidths,_fnThrottle,_fnConvertToWidth,_fnGetWidestNode,_fnGetMaxLenString,_fnStringToCss,_fnSortFlatten,_fnSort,_fnSortAria,_fnSortListener,_fnSortAttachListener,_fnSortingClasses,_fnSortData,_fnSaveState,_fnLoadState,_fnSettingsFromNode,_fnLog,_fnMap,_fnBindAction,_fnCallbackReg,_fnCallbackFire,_fnLengthOverflow,_fnRenderer,_fnDataSource,_fnRowAttributes*/ + +(function( factory ) { + "use strict"; + + if ( typeof define === 'function' && define.amd ) { + // AMD + define( ['jquery'], function ( $ ) { + return factory( $, window, document ); + } ); + } + else if ( typeof exports === 'object' ) { + // CommonJS + module.exports = function (root, $) { + if ( ! root ) { + // CommonJS environments without a window global must pass a + // root. This will give an error otherwise + root = window; + } + + if ( ! $ ) { + $ = typeof window !== 'undefined' ? // jQuery's factory checks for a global window + require('jquery') : + require('jquery')( root ); + } + + return factory( $, root, root.document ); + }; + } + else { + // Browser + window.DataTable = factory( jQuery, window, document ); + } +} +(function( $, window, document, undefined ) { + "use strict"; + + + var DataTable = function ( selector, options ) + { + // When creating with `new`, create a new DataTable, returning the API instance + if (this instanceof DataTable) { + return $(selector).DataTable(options); + } + else { + // Argument switching + options = selector; + } + + /** + * Perform a jQuery selector action on the table's TR elements (from the tbody) and + * return the resulting jQuery object. + * @param {string|node|jQuery} sSelector jQuery selector or node collection to act on + * @param {object} [oOpts] Optional parameters for modifying the rows to be included + * @param {string} [oOpts.filter=none] Select TR elements that meet the current filter + * criterion ("applied") or all TR elements (i.e. no filter). + * @param {string} [oOpts.order=current] Order of the TR elements in the processed array. + * Can be either 'current', whereby the current sorting of the table is used, or + * 'original' whereby the original order the data was read into the table is used. + * @param {string} [oOpts.page=all] Limit the selection to the currently displayed page + * ("current") or not ("all"). If 'current' is given, then order is assumed to be + * 'current' and filter is 'applied', regardless of what they might be given as. + * @returns {object} jQuery object, filtered by the given selector. + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Highlight every second row + * oTable.$('tr:odd').css('backgroundColor', 'blue'); + * } ); + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Filter to rows with 'Webkit' in them, add a background colour and then + * // remove the filter, thus highlighting the 'Webkit' rows only. + * oTable.fnFilter('Webkit'); + * oTable.$('tr', {"search": "applied"}).css('backgroundColor', 'blue'); + * oTable.fnFilter(''); + * } ); + */ + this.$ = function ( sSelector, oOpts ) + { + return this.api(true).$( sSelector, oOpts ); + }; + + + /** + * Almost identical to $ in operation, but in this case returns the data for the matched + * rows - as such, the jQuery selector used should match TR row nodes or TD/TH cell nodes + * rather than any descendants, so the data can be obtained for the row/cell. If matching + * rows are found, the data returned is the original data array/object that was used to + * create the row (or a generated array if from a DOM source). + * + * This method is often useful in-combination with $ where both functions are given the + * same parameters and the array indexes will match identically. + * @param {string|node|jQuery} sSelector jQuery selector or node collection to act on + * @param {object} [oOpts] Optional parameters for modifying the rows to be included + * @param {string} [oOpts.filter=none] Select elements that meet the current filter + * criterion ("applied") or all elements (i.e. no filter). + * @param {string} [oOpts.order=current] Order of the data in the processed array. + * Can be either 'current', whereby the current sorting of the table is used, or + * 'original' whereby the original order the data was read into the table is used. + * @param {string} [oOpts.page=all] Limit the selection to the currently displayed page + * ("current") or not ("all"). If 'current' is given, then order is assumed to be + * 'current' and filter is 'applied', regardless of what they might be given as. + * @returns {array} Data for the matched elements. If any elements, as a result of the + * selector, were not TR, TD or TH elements in the DataTable, they will have a null + * entry in the array. + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Get the data from the first row in the table + * var data = oTable._('tr:first'); + * + * // Do something useful with the data + * alert( "First cell is: "+data[0] ); + * } ); + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Filter to 'Webkit' and get all data for + * oTable.fnFilter('Webkit'); + * var data = oTable._('tr', {"search": "applied"}); + * + * // Do something with the data + * alert( data.length+" rows matched the search" ); + * } ); + */ + this._ = function ( sSelector, oOpts ) + { + return this.api(true).rows( sSelector, oOpts ).data(); + }; + + + /** + * Create a DataTables Api instance, with the currently selected tables for + * the Api's context. + * @param {boolean} [traditional=false] Set the API instance's context to be + * only the table referred to by the `DataTable.ext.iApiIndex` option, as was + * used in the API presented by DataTables 1.9- (i.e. the traditional mode), + * or if all tables captured in the jQuery object should be used. + * @return {DataTables.Api} + */ + this.api = function ( traditional ) + { + return traditional ? + new _Api( + _fnSettingsFromNode( this[ _ext.iApiIndex ] ) + ) : + new _Api( this ); + }; + + + /** + * Add a single new row or multiple rows of data to the table. Please note + * that this is suitable for client-side processing only - if you are using + * server-side processing (i.e. "bServerSide": true), then to add data, you + * must add it to the data source, i.e. the server-side, through an Ajax call. + * @param {array|object} data The data to be added to the table. This can be: + *
      + *
    • 1D array of data - add a single row with the data provided
    • + *
    • 2D array of arrays - add multiple rows in a single call
    • + *
    • object - data object when using mData
    • + *
    • array of objects - multiple data objects when using mData
    • + *
    + * @param {bool} [redraw=true] redraw the table or not + * @returns {array} An array of integers, representing the list of indexes in + * aoData ({@link DataTable.models.oSettings}) that have been added to + * the table. + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * // Global var for counter + * var giCount = 2; + * + * $(document).ready(function() { + * $('#example').dataTable(); + * } ); + * + * function fnClickAddRow() { + * $('#example').dataTable().fnAddData( [ + * giCount+".1", + * giCount+".2", + * giCount+".3", + * giCount+".4" ] + * ); + * + * giCount++; + * } + */ + this.fnAddData = function( data, redraw ) + { + var api = this.api( true ); + + /* Check if we want to add multiple rows or not */ + var rows = Array.isArray(data) && ( Array.isArray(data[0]) || $.isPlainObject(data[0]) ) ? + api.rows.add( data ) : + api.row.add( data ); + + if ( redraw === undefined || redraw ) { + api.draw(); + } + + return rows.flatten().toArray(); + }; + + + /** + * This function will make DataTables recalculate the column sizes, based on the data + * contained in the table and the sizes applied to the columns (in the DOM, CSS or + * through the sWidth parameter). This can be useful when the width of the table's + * parent element changes (for example a window resize). + * @param {boolean} [bRedraw=true] Redraw the table or not, you will typically want to + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable( { + * "sScrollY": "200px", + * "bPaginate": false + * } ); + * + * $(window).on('resize', function () { + * oTable.fnAdjustColumnSizing(); + * } ); + * } ); + */ + this.fnAdjustColumnSizing = function ( bRedraw ) + { + var api = this.api( true ).columns.adjust(); + var settings = api.settings()[0]; + var scroll = settings.oScroll; + + if ( bRedraw === undefined || bRedraw ) { + api.draw( false ); + } + else if ( scroll.sX !== "" || scroll.sY !== "" ) { + /* If not redrawing, but scrolling, we want to apply the new column sizes anyway */ + _fnScrollDraw( settings ); + } + }; + + + /** + * Quickly and simply clear a table + * @param {bool} [bRedraw=true] redraw the table or not + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Immediately 'nuke' the current rows (perhaps waiting for an Ajax callback...) + * oTable.fnClearTable(); + * } ); + */ + this.fnClearTable = function( bRedraw ) + { + var api = this.api( true ).clear(); + + if ( bRedraw === undefined || bRedraw ) { + api.draw(); + } + }; + + + /** + * The exact opposite of 'opening' a row, this function will close any rows which + * are currently 'open'. + * @param {node} nTr the table row to 'close' + * @returns {int} 0 on success, or 1 if failed (can't find the row) + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable; + * + * // 'open' an information row when a row is clicked on + * $('#example tbody tr').click( function () { + * if ( oTable.fnIsOpen(this) ) { + * oTable.fnClose( this ); + * } else { + * oTable.fnOpen( this, "Temporary row opened", "info_row" ); + * } + * } ); + * + * oTable = $('#example').dataTable(); + * } ); + */ + this.fnClose = function( nTr ) + { + this.api( true ).row( nTr ).child.hide(); + }; + + + /** + * Remove a row for the table + * @param {mixed} target The index of the row from aoData to be deleted, or + * the TR element you want to delete + * @param {function|null} [callBack] Callback function + * @param {bool} [redraw=true] Redraw the table or not + * @returns {array} The row that was deleted + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Immediately remove the first row + * oTable.fnDeleteRow( 0 ); + * } ); + */ + this.fnDeleteRow = function( target, callback, redraw ) + { + var api = this.api( true ); + var rows = api.rows( target ); + var settings = rows.settings()[0]; + var data = settings.aoData[ rows[0][0] ]; + + rows.remove(); + + if ( callback ) { + callback.call( this, settings, data ); + } + + if ( redraw === undefined || redraw ) { + api.draw(); + } + + return data; + }; + + + /** + * Restore the table to it's original state in the DOM by removing all of DataTables + * enhancements, alterations to the DOM structure of the table and event listeners. + * @param {boolean} [remove=false] Completely remove the table from the DOM + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * // This example is fairly pointless in reality, but shows how fnDestroy can be used + * var oTable = $('#example').dataTable(); + * oTable.fnDestroy(); + * } ); + */ + this.fnDestroy = function ( remove ) + { + this.api( true ).destroy( remove ); + }; + + + /** + * Redraw the table + * @param {bool} [complete=true] Re-filter and resort (if enabled) the table before the draw. + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Re-draw the table - you wouldn't want to do it here, but it's an example :-) + * oTable.fnDraw(); + * } ); + */ + this.fnDraw = function( complete ) + { + // Note that this isn't an exact match to the old call to _fnDraw - it takes + // into account the new data, but can hold position. + this.api( true ).draw( complete ); + }; + + + /** + * Filter the input based on data + * @param {string} sInput String to filter the table on + * @param {int|null} [iColumn] Column to limit filtering to + * @param {bool} [bRegex=false] Treat as regular expression or not + * @param {bool} [bSmart=true] Perform smart filtering or not + * @param {bool} [bShowGlobal=true] Show the input global filter in it's input box(es) + * @param {bool} [bCaseInsensitive=true] Do case-insensitive matching (true) or not (false) + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Sometime later - filter... + * oTable.fnFilter( 'test string' ); + * } ); + */ + this.fnFilter = function( sInput, iColumn, bRegex, bSmart, bShowGlobal, bCaseInsensitive ) + { + var api = this.api( true ); + + if ( iColumn === null || iColumn === undefined ) { + api.search( sInput, bRegex, bSmart, bCaseInsensitive ); + } + else { + api.column( iColumn ).search( sInput, bRegex, bSmart, bCaseInsensitive ); + } + + api.draw(); + }; + + + /** + * Get the data for the whole table, an individual row or an individual cell based on the + * provided parameters. + * @param {int|node} [src] A TR row node, TD/TH cell node or an integer. If given as + * a TR node then the data source for the whole row will be returned. If given as a + * TD/TH cell node then iCol will be automatically calculated and the data for the + * cell returned. If given as an integer, then this is treated as the aoData internal + * data index for the row (see fnGetPosition) and the data for that row used. + * @param {int} [col] Optional column index that you want the data of. + * @returns {array|object|string} If mRow is undefined, then the data for all rows is + * returned. If mRow is defined, just data for that row, and is iCol is + * defined, only data for the designated cell is returned. + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * // Row data + * $(document).ready(function() { + * oTable = $('#example').dataTable(); + * + * oTable.$('tr').click( function () { + * var data = oTable.fnGetData( this ); + * // ... do something with the array / object of data for the row + * } ); + * } ); + * + * @example + * // Individual cell data + * $(document).ready(function() { + * oTable = $('#example').dataTable(); + * + * oTable.$('td').click( function () { + * var sData = oTable.fnGetData( this ); + * alert( 'The cell clicked on had the value of '+sData ); + * } ); + * } ); + */ + this.fnGetData = function( src, col ) + { + var api = this.api( true ); + + if ( src !== undefined ) { + var type = src.nodeName ? src.nodeName.toLowerCase() : ''; + + return col !== undefined || type == 'td' || type == 'th' ? + api.cell( src, col ).data() : + api.row( src ).data() || null; + } + + return api.data().toArray(); + }; + + + /** + * Get an array of the TR nodes that are used in the table's body. Note that you will + * typically want to use the '$' API method in preference to this as it is more + * flexible. + * @param {int} [iRow] Optional row index for the TR element you want + * @returns {array|node} If iRow is undefined, returns an array of all TR elements + * in the table's body, or iRow is defined, just the TR element requested. + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Get the nodes from the table + * var nNodes = oTable.fnGetNodes( ); + * } ); + */ + this.fnGetNodes = function( iRow ) + { + var api = this.api( true ); + + return iRow !== undefined ? + api.row( iRow ).node() : + api.rows().nodes().flatten().toArray(); + }; + + + /** + * Get the array indexes of a particular cell from it's DOM element + * and column index including hidden columns + * @param {node} node this can either be a TR, TD or TH in the table's body + * @returns {int} If nNode is given as a TR, then a single index is returned, or + * if given as a cell, an array of [row index, column index (visible), + * column index (all)] is given. + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * $('#example tbody td').click( function () { + * // Get the position of the current data from the node + * var aPos = oTable.fnGetPosition( this ); + * + * // Get the data array for this row + * var aData = oTable.fnGetData( aPos[0] ); + * + * // Update the data array and return the value + * aData[ aPos[1] ] = 'clicked'; + * this.innerHTML = 'clicked'; + * } ); + * + * // Init DataTables + * oTable = $('#example').dataTable(); + * } ); + */ + this.fnGetPosition = function( node ) + { + var api = this.api( true ); + var nodeName = node.nodeName.toUpperCase(); + + if ( nodeName == 'TR' ) { + return api.row( node ).index(); + } + else if ( nodeName == 'TD' || nodeName == 'TH' ) { + var cell = api.cell( node ).index(); + + return [ + cell.row, + cell.columnVisible, + cell.column + ]; + } + return null; + }; + + + /** + * Check to see if a row is 'open' or not. + * @param {node} nTr the table row to check + * @returns {boolean} true if the row is currently open, false otherwise + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable; + * + * // 'open' an information row when a row is clicked on + * $('#example tbody tr').click( function () { + * if ( oTable.fnIsOpen(this) ) { + * oTable.fnClose( this ); + * } else { + * oTable.fnOpen( this, "Temporary row opened", "info_row" ); + * } + * } ); + * + * oTable = $('#example').dataTable(); + * } ); + */ + this.fnIsOpen = function( nTr ) + { + return this.api( true ).row( nTr ).child.isShown(); + }; + + + /** + * This function will place a new row directly after a row which is currently + * on display on the page, with the HTML contents that is passed into the + * function. This can be used, for example, to ask for confirmation that a + * particular record should be deleted. + * @param {node} nTr The table row to 'open' + * @param {string|node|jQuery} mHtml The HTML to put into the row + * @param {string} sClass Class to give the new TD cell + * @returns {node} The row opened. Note that if the table row passed in as the + * first parameter, is not found in the table, this method will silently + * return. + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable; + * + * // 'open' an information row when a row is clicked on + * $('#example tbody tr').click( function () { + * if ( oTable.fnIsOpen(this) ) { + * oTable.fnClose( this ); + * } else { + * oTable.fnOpen( this, "Temporary row opened", "info_row" ); + * } + * } ); + * + * oTable = $('#example').dataTable(); + * } ); + */ + this.fnOpen = function( nTr, mHtml, sClass ) + { + return this.api( true ) + .row( nTr ) + .child( mHtml, sClass ) + .show() + .child()[0]; + }; + + + /** + * Change the pagination - provides the internal logic for pagination in a simple API + * function. With this function you can have a DataTables table go to the next, + * previous, first or last pages. + * @param {string|int} mAction Paging action to take: "first", "previous", "next" or "last" + * or page number to jump to (integer), note that page 0 is the first page. + * @param {bool} [bRedraw=true] Redraw the table or not + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * oTable.fnPageChange( 'next' ); + * } ); + */ + this.fnPageChange = function ( mAction, bRedraw ) + { + var api = this.api( true ).page( mAction ); + + if ( bRedraw === undefined || bRedraw ) { + api.draw(false); + } + }; + + + /** + * Show a particular column + * @param {int} iCol The column whose display should be changed + * @param {bool} bShow Show (true) or hide (false) the column + * @param {bool} [bRedraw=true] Redraw the table or not + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Hide the second column after initialisation + * oTable.fnSetColumnVis( 1, false ); + * } ); + */ + this.fnSetColumnVis = function ( iCol, bShow, bRedraw ) + { + var api = this.api( true ).column( iCol ).visible( bShow ); + + if ( bRedraw === undefined || bRedraw ) { + api.columns.adjust().draw(); + } + }; + + + /** + * Get the settings for a particular table for external manipulation + * @returns {object} DataTables settings object. See + * {@link DataTable.models.oSettings} + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * var oSettings = oTable.fnSettings(); + * + * // Show an example parameter from the settings + * alert( oSettings._iDisplayStart ); + * } ); + */ + this.fnSettings = function() + { + return _fnSettingsFromNode( this[_ext.iApiIndex] ); + }; + + + /** + * Sort the table by a particular column + * @param {int} iCol the data index to sort on. Note that this will not match the + * 'display index' if you have hidden data entries + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Sort immediately with columns 0 and 1 + * oTable.fnSort( [ [0,'asc'], [1,'asc'] ] ); + * } ); + */ + this.fnSort = function( aaSort ) + { + this.api( true ).order( aaSort ).draw(); + }; + + + /** + * Attach a sort listener to an element for a given column + * @param {node} nNode the element to attach the sort listener to + * @param {int} iColumn the column that a click on this node will sort on + * @param {function} [fnCallback] callback function when sort is run + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Sort on column 1, when 'sorter' is clicked on + * oTable.fnSortListener( document.getElementById('sorter'), 1 ); + * } ); + */ + this.fnSortListener = function( nNode, iColumn, fnCallback ) + { + this.api( true ).order.listener( nNode, iColumn, fnCallback ); + }; + + + /** + * Update a table cell or row - this method will accept either a single value to + * update the cell with, an array of values with one element for each column or + * an object in the same format as the original data source. The function is + * self-referencing in order to make the multi column updates easier. + * @param {object|array|string} mData Data to update the cell/row with + * @param {node|int} mRow TR element you want to update or the aoData index + * @param {int} [iColumn] The column to update, give as null or undefined to + * update a whole row. + * @param {bool} [bRedraw=true] Redraw the table or not + * @param {bool} [bAction=true] Perform pre-draw actions or not + * @returns {int} 0 on success, 1 on error + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * oTable.fnUpdate( 'Example update', 0, 0 ); // Single cell + * oTable.fnUpdate( ['a', 'b', 'c', 'd', 'e'], $('tbody tr')[0] ); // Row + * } ); + */ + this.fnUpdate = function( mData, mRow, iColumn, bRedraw, bAction ) + { + var api = this.api( true ); + + if ( iColumn === undefined || iColumn === null ) { + api.row( mRow ).data( mData ); + } + else { + api.cell( mRow, iColumn ).data( mData ); + } + + if ( bAction === undefined || bAction ) { + api.columns.adjust(); + } + + if ( bRedraw === undefined || bRedraw ) { + api.draw(); + } + return 0; + }; + + + /** + * Provide a common method for plug-ins to check the version of DataTables being used, in order + * to ensure compatibility. + * @param {string} sVersion Version string to check for, in the format "X.Y.Z". Note that the + * formats "X" and "X.Y" are also acceptable. + * @returns {boolean} true if this version of DataTables is greater or equal to the required + * version, or false if this version of DataTales is not suitable + * @method + * @dtopt API + * @deprecated Since v1.10 + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * alert( oTable.fnVersionCheck( '1.9.0' ) ); + * } ); + */ + this.fnVersionCheck = _ext.fnVersionCheck; + + + var _that = this; + var emptyInit = options === undefined; + var len = this.length; + + if ( emptyInit ) { + options = {}; + } + + this.oApi = this.internal = _ext.internal; + + // Extend with old style plug-in API methods + for ( var fn in DataTable.ext.internal ) { + if ( fn ) { + this[fn] = _fnExternApiFunc(fn); + } + } + + this.each(function() { + // For each initialisation we want to give it a clean initialisation + // object that can be bashed around + var o = {}; + var oInit = len > 1 ? // optimisation for single table case + _fnExtend( o, options, true ) : + options; + + /*global oInit,_that,emptyInit*/ + var i=0, iLen, j, jLen, k, kLen; + var sId = this.getAttribute( 'id' ); + var bInitHandedOff = false; + var defaults = DataTable.defaults; + var $this = $(this); + + + /* Sanity check */ + if ( this.nodeName.toLowerCase() != 'table' ) + { + _fnLog( null, 0, 'Non-table node initialisation ('+this.nodeName+')', 2 ); + return; + } + + /* Backwards compatibility for the defaults */ + _fnCompatOpts( defaults ); + _fnCompatCols( defaults.column ); + + /* Convert the camel-case defaults to Hungarian */ + _fnCamelToHungarian( defaults, defaults, true ); + _fnCamelToHungarian( defaults.column, defaults.column, true ); + + /* Setting up the initialisation object */ + _fnCamelToHungarian( defaults, $.extend( oInit, $this.data() ), true ); + + + + /* Check to see if we are re-initialising a table */ + var allSettings = DataTable.settings; + for ( i=0, iLen=allSettings.length ; i').appendTo($this); + } + oSettings.nTHead = thead[0]; + + var tbody = $this.children('tbody'); + if ( tbody.length === 0 ) { + tbody = $('').insertAfter(thead); + } + oSettings.nTBody = tbody[0]; + + var tfoot = $this.children('tfoot'); + if ( tfoot.length === 0 && captions.length > 0 && (oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "") ) { + // If we are a scrolling table, and no footer has been given, then we need to create + // a tfoot element for the caption element to be appended to + tfoot = $('').appendTo($this); + } + + if ( tfoot.length === 0 || tfoot.children().length === 0 ) { + $this.addClass( oClasses.sNoFooter ); + } + else if ( tfoot.length > 0 ) { + oSettings.nTFoot = tfoot[0]; + _fnDetectHeader( oSettings.aoFooter, oSettings.nTFoot ); + } + + /* Check if there is data passing into the constructor */ + if ( oInit.aaData ) { + for ( i=0 ; i/g; + + // This is not strict ISO8601 - Date.parse() is quite lax, although + // implementations differ between browsers. + var _re_date = /^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/; + + // Escape regular expression special characters + var _re_escape_regex = new RegExp( '(\\' + [ '/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\', '$', '^', '-' ].join('|\\') + ')', 'g' ); + + // http://en.wikipedia.org/wiki/Foreign_exchange_market + // - \u20BD - Russian ruble. + // - \u20a9 - South Korean Won + // - \u20BA - Turkish Lira + // - \u20B9 - Indian Rupee + // - R - Brazil (R$) and South Africa + // - fr - Swiss Franc + // - kr - Swedish krona, Norwegian krone and Danish krone + // - \u2009 is thin space and \u202F is narrow no-break space, both used in many + // - Ƀ - Bitcoin + // - Ξ - Ethereum + // standards as thousands separators. + var _re_formatted_numeric = /['\u00A0,$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfkɃΞ]/gi; + + + var _empty = function ( d ) { + return !d || d === true || d === '-' ? true : false; + }; + + + var _intVal = function ( s ) { + var integer = parseInt( s, 10 ); + return !isNaN(integer) && isFinite(s) ? integer : null; + }; + + // Convert from a formatted number with characters other than `.` as the + // decimal place, to a Javascript number + var _numToDecimal = function ( num, decimalPoint ) { + // Cache created regular expressions for speed as this function is called often + if ( ! _re_dic[ decimalPoint ] ) { + _re_dic[ decimalPoint ] = new RegExp( _fnEscapeRegex( decimalPoint ), 'g' ); + } + return typeof num === 'string' && decimalPoint !== '.' ? + num.replace( /\./g, '' ).replace( _re_dic[ decimalPoint ], '.' ) : + num; + }; + + + var _isNumber = function ( d, decimalPoint, formatted ) { + var strType = typeof d === 'string'; + + // If empty return immediately so there must be a number if it is a + // formatted string (this stops the string "k", or "kr", etc being detected + // as a formatted number for currency + if ( _empty( d ) ) { + return true; + } + + if ( decimalPoint && strType ) { + d = _numToDecimal( d, decimalPoint ); + } + + if ( formatted && strType ) { + d = d.replace( _re_formatted_numeric, '' ); + } + + return !isNaN( parseFloat(d) ) && isFinite( d ); + }; + + + // A string without HTML in it can be considered to be HTML still + var _isHtml = function ( d ) { + return _empty( d ) || typeof d === 'string'; + }; + + + var _htmlNumeric = function ( d, decimalPoint, formatted ) { + if ( _empty( d ) ) { + return true; + } + + var html = _isHtml( d ); + return ! html ? + null : + _isNumber( _stripHtml( d ), decimalPoint, formatted ) ? + true : + null; + }; + + + var _pluck = function ( a, prop, prop2 ) { + var out = []; + var i=0, ien=a.length; + + // Could have the test in the loop for slightly smaller code, but speed + // is essential here + if ( prop2 !== undefined ) { + for ( ; i') + .css( { + position: 'fixed', + top: 0, + left: $(window).scrollLeft()*-1, // allow for scrolling + height: 1, + width: 1, + overflow: 'hidden' + } ) + .append( + $('
    ') + .css( { + position: 'absolute', + top: 1, + left: 1, + width: 100, + overflow: 'scroll' + } ) + .append( + $('
    ') + .css( { + width: '100%', + height: 10 + } ) + ) + ) + .appendTo( 'body' ); + + var outer = n.children(); + var inner = outer.children(); + + // Numbers below, in order, are: + // inner.offsetWidth, inner.clientWidth, outer.offsetWidth, outer.clientWidth + // + // IE6 XP: 100 100 100 83 + // IE7 Vista: 100 100 100 83 + // IE 8+ Windows: 83 83 100 83 + // Evergreen Windows: 83 83 100 83 + // Evergreen Mac with scrollbars: 85 85 100 85 + // Evergreen Mac without scrollbars: 100 100 100 100 + + // Get scrollbar width + browser.barWidth = outer[0].offsetWidth - outer[0].clientWidth; + + // IE6/7 will oversize a width 100% element inside a scrolling element, to + // include the width of the scrollbar, while other browsers ensure the inner + // element is contained without forcing scrolling + browser.bScrollOversize = inner[0].offsetWidth === 100 && outer[0].clientWidth !== 100; + + // In rtl text layout, some browsers (most, but not all) will place the + // scrollbar on the left, rather than the right. + browser.bScrollbarLeft = Math.round( inner.offset().left ) !== 1; + + // IE8- don't provide height and width for getBoundingClientRect + browser.bBounding = n[0].getBoundingClientRect().width ? true : false; + + n.remove(); + } + + $.extend( settings.oBrowser, DataTable.__browser ); + settings.oScroll.iBarWidth = DataTable.__browser.barWidth; + } + + + /** + * Array.prototype reduce[Right] method, used for browsers which don't support + * JS 1.6. Done this way to reduce code size, since we iterate either way + * @param {object} settings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnReduce ( that, fn, init, start, end, inc ) + { + var + i = start, + value, + isSet = false; + + if ( init !== undefined ) { + value = init; + isSet = true; + } + + while ( i !== end ) { + if ( ! that.hasOwnProperty(i) ) { + continue; + } + + value = isSet ? + fn( value, that[i], i, that ) : + that[i]; + + isSet = true; + i += inc; + } + + return value; + } + + /** + * Add a column to the list used for the table with default values + * @param {object} oSettings dataTables settings object + * @param {node} nTh The th element for this column + * @memberof DataTable#oApi + */ + function _fnAddColumn( oSettings, nTh ) + { + // Add column to aoColumns array + var oDefaults = DataTable.defaults.column; + var iCol = oSettings.aoColumns.length; + var oCol = $.extend( {}, DataTable.models.oColumn, oDefaults, { + "nTh": nTh ? nTh : document.createElement('th'), + "sTitle": oDefaults.sTitle ? oDefaults.sTitle : nTh ? nTh.innerHTML : '', + "aDataSort": oDefaults.aDataSort ? oDefaults.aDataSort : [iCol], + "mData": oDefaults.mData ? oDefaults.mData : iCol, + idx: iCol + } ); + oSettings.aoColumns.push( oCol ); + + // Add search object for column specific search. Note that the `searchCols[ iCol ]` + // passed into extend can be undefined. This allows the user to give a default + // with only some of the parameters defined, and also not give a default + var searchCols = oSettings.aoPreSearchCols; + searchCols[ iCol ] = $.extend( {}, DataTable.models.oSearch, searchCols[ iCol ] ); + + // Use the default column options function to initialise classes etc + _fnColumnOptions( oSettings, iCol, $(nTh).data() ); + } + + + /** + * Apply options for a column + * @param {object} oSettings dataTables settings object + * @param {int} iCol column index to consider + * @param {object} oOptions object with sType, bVisible and bSearchable etc + * @memberof DataTable#oApi + */ + function _fnColumnOptions( oSettings, iCol, oOptions ) + { + var oCol = oSettings.aoColumns[ iCol ]; + var oClasses = oSettings.oClasses; + var th = $(oCol.nTh); + + // Try to get width information from the DOM. We can't get it from CSS + // as we'd need to parse the CSS stylesheet. `width` option can override + if ( ! oCol.sWidthOrig ) { + // Width attribute + oCol.sWidthOrig = th.attr('width') || null; + + // Style attribute + var t = (th.attr('style') || '').match(/width:\s*(\d+[pxem%]+)/); + if ( t ) { + oCol.sWidthOrig = t[1]; + } + } + + /* User specified column options */ + if ( oOptions !== undefined && oOptions !== null ) + { + // Backwards compatibility + _fnCompatCols( oOptions ); + + // Map camel case parameters to their Hungarian counterparts + _fnCamelToHungarian( DataTable.defaults.column, oOptions, true ); + + /* Backwards compatibility for mDataProp */ + if ( oOptions.mDataProp !== undefined && !oOptions.mData ) + { + oOptions.mData = oOptions.mDataProp; + } + + if ( oOptions.sType ) + { + oCol._sManualType = oOptions.sType; + } + + // `class` is a reserved word in Javascript, so we need to provide + // the ability to use a valid name for the camel case input + if ( oOptions.className && ! oOptions.sClass ) + { + oOptions.sClass = oOptions.className; + } + if ( oOptions.sClass ) { + th.addClass( oOptions.sClass ); + } + + var origClass = oCol.sClass; + + $.extend( oCol, oOptions ); + _fnMap( oCol, oOptions, "sWidth", "sWidthOrig" ); + + // Merge class from previously defined classes with this one, rather than just + // overwriting it in the extend above + if (origClass !== oCol.sClass) { + oCol.sClass = origClass + ' ' + oCol.sClass; + } + + /* iDataSort to be applied (backwards compatibility), but aDataSort will take + * priority if defined + */ + if ( oOptions.iDataSort !== undefined ) + { + oCol.aDataSort = [ oOptions.iDataSort ]; + } + _fnMap( oCol, oOptions, "aDataSort" ); + } + + /* Cache the data get and set functions for speed */ + var mDataSrc = oCol.mData; + var mData = _fnGetObjectDataFn( mDataSrc ); + var mRender = oCol.mRender ? _fnGetObjectDataFn( oCol.mRender ) : null; + + var attrTest = function( src ) { + return typeof src === 'string' && src.indexOf('@') !== -1; + }; + oCol._bAttrSrc = $.isPlainObject( mDataSrc ) && ( + attrTest(mDataSrc.sort) || attrTest(mDataSrc.type) || attrTest(mDataSrc.filter) + ); + oCol._setter = null; + + oCol.fnGetData = function (rowData, type, meta) { + var innerData = mData( rowData, type, undefined, meta ); + + return mRender && type ? + mRender( innerData, type, rowData, meta ) : + innerData; + }; + oCol.fnSetData = function ( rowData, val, meta ) { + return _fnSetObjectDataFn( mDataSrc )( rowData, val, meta ); + }; + + // Indicate if DataTables should read DOM data as an object or array + // Used in _fnGetRowElements + if ( typeof mDataSrc !== 'number' ) { + oSettings._rowReadObject = true; + } + + /* Feature sorting overrides column specific when off */ + if ( !oSettings.oFeatures.bSort ) + { + oCol.bSortable = false; + th.addClass( oClasses.sSortableNone ); // Have to add class here as order event isn't called + } + + /* Check that the class assignment is correct for sorting */ + var bAsc = $.inArray('asc', oCol.asSorting) !== -1; + var bDesc = $.inArray('desc', oCol.asSorting) !== -1; + if ( !oCol.bSortable || (!bAsc && !bDesc) ) + { + oCol.sSortingClass = oClasses.sSortableNone; + oCol.sSortingClassJUI = ""; + } + else if ( bAsc && !bDesc ) + { + oCol.sSortingClass = oClasses.sSortableAsc; + oCol.sSortingClassJUI = oClasses.sSortJUIAscAllowed; + } + else if ( !bAsc && bDesc ) + { + oCol.sSortingClass = oClasses.sSortableDesc; + oCol.sSortingClassJUI = oClasses.sSortJUIDescAllowed; + } + else + { + oCol.sSortingClass = oClasses.sSortable; + oCol.sSortingClassJUI = oClasses.sSortJUI; + } + } + + + /** + * Adjust the table column widths for new data. Note: you would probably want to + * do a redraw after calling this function! + * @param {object} settings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnAdjustColumnSizing ( settings ) + { + /* Not interested in doing column width calculation if auto-width is disabled */ + if ( settings.oFeatures.bAutoWidth !== false ) + { + var columns = settings.aoColumns; + + _fnCalculateColumnWidths( settings ); + for ( var i=0 , iLen=columns.length ; i
  • + + + + + + + +
    +
    +
    + +
    +
    +
    +
    +

    {{ lang.edit.quota_warning_bcc }}

    +

    {{ lang.edit.quota_warning_bcc_info|raw }}

    +
    + +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    + +
    +
    + + +{% else %} + {{ parent() }} +{% endif %} +{% endblock %} diff --git a/mailcow/data/web/templates/edit/domainadmin.twig b/mailcow/data/web/templates/edit/domainadmin.twig new file mode 100644 index 0000000..2c40faa --- /dev/null +++ b/mailcow/data/web/templates/edit/domainadmin.twig @@ -0,0 +1,79 @@ +{% extends 'edit.twig' %} + +{% block inner_content %} +{% if result %} +

    {{ lang.edit.domain_admin }}

    +
    +
    + +
    + +
    + + ↳ a-z - _ . +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +

    ACL

    +
    +
    + + +
    +
    +
    +{% else %} + {{ parent() }} +{% endif %} +{% endblock %} diff --git a/mailcow/data/web/templates/edit/filter.twig b/mailcow/data/web/templates/edit/filter.twig new file mode 100644 index 0000000..124ecaf --- /dev/null +++ b/mailcow/data/web/templates/edit/filter.twig @@ -0,0 +1,45 @@ +{% extends 'edit.twig' %} + +{% block inner_content %} +{% if result %} +

    Filter

    +
    + +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +{% else %} + {{ parent() }} +{% endif %} +{% endblock %} diff --git a/mailcow/data/web/templates/edit/mailbox-templates.twig b/mailcow/data/web/templates/edit/mailbox-templates.twig new file mode 100644 index 0000000..6d150b2 --- /dev/null +++ b/mailcow/data/web/templates/edit/mailbox-templates.twig @@ -0,0 +1,169 @@ +{% extends 'edit.twig' %} + +{% block inner_content %} +{% if result %} +
    +
    +
    + + + + + + +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + + + +
    +
    +
    +
    + +
    + + 0 = ∞ +
    +
    +
    + +
    +
    + + + + + + + + + + + +
    +

    {{ lang.user.quarantine_notification_info }}

    +
    +
    +
    + +
    +
    + + + + + + + + +
    +

    {{ lang.user.quarantine_category_info }}

    +
    +
    +
    + +
    +
    + + + + + +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    + + +
    +

    {{ lang.edit.mbox_rl_info }}

    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + + {{ lang.edit.force_pw_update_info|format(ui_texts.main_name) }} +
    +
    +
    + {% if not skip_sogo %} +
    +
    +
    + + {{ lang.edit.sogo_access_info }} +
    +
    +
    + {% endif %} +
    +
    + +
    +
    +
    +
    +{% else %} + {{ parent() }} +{% endif %} +{% endblock %} diff --git a/mailcow/data/web/templates/edit/mailbox.twig b/mailcow/data/web/templates/edit/mailbox.twig new file mode 100644 index 0000000..32a3706 --- /dev/null +++ b/mailcow/data/web/templates/edit/mailbox.twig @@ -0,0 +1,559 @@ +{% extends 'edit.twig' %} + +{% block inner_content %} +{% if result %} +
    + +
    +
    +
    +
    +
    + +
    +
    +
    + + + + +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    + {% for tag in mailbox_details.tags %} + + + {{ tag }} + + {% endfor %} + + + +
    +
    +
    +
    + +
    + + 0 = ∞ +
    +
    +
    + +
    + +
    {{ lang.edit.sender_acl_disabled|raw }}
    + {{ lang.edit.sender_acl_info|raw }} +
    +
    +
    + +
    + +

     

    + {{ lang.edit.mailbox_relayhost_info }} +
    +
    +
    + +
    +
    + + + + +
    +

    {{ lang.user.quarantine_notification_info }}

    +
    +
    +
    + +
    +
    + + + +
    +

    {{ lang.user.quarantine_category_info }}

    +
    +
    +
    + +
    +
    + + +
    +
    +
    + {% if not result.authsource or result.authsource == 'mailcow' %} +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + + {{ lang.admin.password_reset_info }} +
    +
    +
    + +
    + {% if sender_acl_handles.external_sender_aliases %} + {% set ext_sender_acl = sender_acl_handles.external_sender_aliases|join(', ') %} + {% endif %} + {% if acl.extend_sender_acl and acl.extend_sender_acl == 1 %} + + {{ lang.edit.extended_sender_acl_info|raw }} + {% endif %} +
    +
    + {% endif %} +
    + +
    + +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    + + {{ lang.edit.force_pw_update_info|format(ui_texts.main_name) }} +
    +
    +
    + {% if not skip_sogo %} +
    +
    +
    + + {{ lang.edit.sogo_access_info }} +
    +
    +
    + {% endif %} +
    +
    + +
    +
    +
    +
    + {{ lang.edit.created_on }}: {{ result.created }} + {{ lang.edit.last_modified }}: {{ result.modified }} +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + + + + + + + {% for key, val in result.custom_attributes %} + + + + + + {% endfor %} +
    {{ lang.user.attribute }}{{ lang.user.value }} 
    {{ lang.admin.remove_row }}
    +

    + + +

    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + + + +
    +
    +

    +
    +
    +

    {{ lang.user.pushover_info|format(mailbox)|raw }}

    +

    {{ lang.edit.pushover_vars|raw }}: {SUBJECT}, {SENDER}, {SENDER_ADDRESS}, {SENDER_NAME}, {TO_NAME}, {TO_ADDRESS}, {MSG_ID}

    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + + {{ lang.edit.advanced_settings }} + +
    +
    +
    +
    + + +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +

    ACL

    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +

    {{ lang.acl.ratelimit }}

    +
    +
    +
    + + +
    +
    +
    +
    +
    + +

    {{ lang.edit.mbox_rl_info }}

    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +

    {{ lang.edit.mailbox_rename_warning }}

    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + + +
    +
    + {{ lang.edit.mailbox_rename_title }} +
    +
    +
    + + @{{ result.domain }} +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +{% else %} + {{ parent() }} +{% endif %} +{% endblock %} diff --git a/mailcow/data/web/templates/edit/oauth2client.twig b/mailcow/data/web/templates/edit/oauth2client.twig new file mode 100644 index 0000000..f3dc4d0 --- /dev/null +++ b/mailcow/data/web/templates/edit/oauth2client.twig @@ -0,0 +1,40 @@ +{% extends 'edit.twig' %} + +{% block inner_content %} +{% if result %} +

    OAuth2

    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    +
    +{% else %} + {{ parent() }} +{% endif %} +{% endblock %} diff --git a/mailcow/data/web/templates/edit/recipient_map.twig b/mailcow/data/web/templates/edit/recipient_map.twig new file mode 100644 index 0000000..c7f3493 --- /dev/null +++ b/mailcow/data/web/templates/edit/recipient_map.twig @@ -0,0 +1,39 @@ +{% extends 'edit.twig' %} + +{% block inner_content %} +{% if result %} +

    {{ lang.mailbox.recipient_map }}: {{ result.recipient_map_old }}

    +
    +
    + +
    + +
    + + {{ lang.mailbox.recipient_map_old_info }} +
    +
    +
    + +
    + + {{ lang.mailbox.recipient_map_new_info }} +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +{% else %} + {{ parent() }} +{% endif %} +{% endblock %} diff --git a/mailcow/data/web/templates/edit/relayhost.twig b/mailcow/data/web/templates/edit/relayhost.twig new file mode 100644 index 0000000..3187170 --- /dev/null +++ b/mailcow/data/web/templates/edit/relayhost.twig @@ -0,0 +1,43 @@ +{% extends 'edit.twig' %} + +{% block inner_content %} +{% if result %} +

    {{ lang.edit.resource }}

    +
    + +
    + +
    + +

    {{ lang.add.relayhost_wrapped_tls_info|raw }}

    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +{% else %} + {{ parent() }} +{% endif %} +{% endblock %} diff --git a/mailcow/data/web/templates/edit/resource.twig b/mailcow/data/web/templates/edit/resource.twig new file mode 100644 index 0000000..7fdc556 --- /dev/null +++ b/mailcow/data/web/templates/edit/resource.twig @@ -0,0 +1,55 @@ +{% extends 'edit.twig' %} + +{% block inner_content %} +{% if result %} +

    {{ lang.edit.resource }}

    +
    + +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + + + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +{% else %} + {{ parent() }} +{% endif %} +{% endblock %} diff --git a/mailcow/data/web/templates/edit/syncjob.twig b/mailcow/data/web/templates/edit/syncjob.twig new file mode 100644 index 0000000..8010098 --- /dev/null +++ b/mailcow/data/web/templates/edit/syncjob.twig @@ -0,0 +1,168 @@ +{% extends 'edit.twig' %} + +{% block inner_content %} +{% if result %} +

    {{ lang.edit.syncjob }}

    +
    + + + + + + + + +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + + 1-43800 +
    +
    +
    + +
    + +
    +
    +
    + +
    + + 0-32000 +
    +
    +
    + +
    + + 0-125000000 +
    +
    +
    + +
    + + 1-32000 +
    +
    +
    + +
    + + 1-32000 +
    +
    +
    + +
    + +
    +
    +
    + +
    + + {{ lang.add.custom_params_hint }} +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +{% else %} + {{ parent() }} +{% endif %} +{% endblock %} diff --git a/mailcow/data/web/templates/edit/tls_policy_map.twig b/mailcow/data/web/templates/edit/tls_policy_map.twig new file mode 100644 index 0000000..aa89575 --- /dev/null +++ b/mailcow/data/web/templates/edit/tls_policy_map.twig @@ -0,0 +1,49 @@ +{% extends 'edit.twig' %} + +{% block inner_content %} +{% if result %} +

    {{ lang.mailbox.tls_policy_maps }}: {{ result.dest }}

    +
    +
    + +
    + +
    + + {{ lang.mailbox.tls_map_dest_info }} +
    +
    +
    + +
    + +
    +
    +
    + +
    + + {{ lang.mailbox.tls_map_parameters_info }} +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +{% else %} + {{ parent() }} +{% endif %} +{% endblock %} diff --git a/mailcow/data/web/templates/edit/transport.twig b/mailcow/data/web/templates/edit/transport.twig new file mode 100644 index 0000000..4aedb53 --- /dev/null +++ b/mailcow/data/web/templates/edit/transport.twig @@ -0,0 +1,56 @@ +{% extends 'edit.twig' %} + +{% block inner_content %} +{% if result %} +

    {{ lang.edit.resource }}

    +
    + + +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +{% else %} + {{ parent() }} +{% endif %} +{% endblock %} diff --git a/mailcow/data/web/templates/fido2.twig b/mailcow/data/web/templates/fido2.twig new file mode 100644 index 0000000..6157aef --- /dev/null +++ b/mailcow/data/web/templates/fido2.twig @@ -0,0 +1,29 @@ +{% if fido2_data %} + {% for key_info in fido2_data %} + + + {% if fido2_cid == key_info.cid %} + + {% endif %} + {% if key_info.fn %} + {{ key_info.fn }} + {% else %} + {{ key_info.subject }} + {% endif %} + + +
    + + +
    + + + {% endfor %} +{% endif %} diff --git a/mailcow/data/web/templates/mailbox.twig b/mailcow/data/web/templates/mailbox.twig new file mode 100644 index 0000000..f0b1af4 --- /dev/null +++ b/mailcow/data/web/templates/mailbox.twig @@ -0,0 +1,79 @@ +{% extends 'base.twig' %} + +{% block content %} +
    + + +
    +
    +
    + {% include 'mailbox/tab-domains.twig' %} + {% if mailcow_cc_role == 'admin' %} + {% include 'mailbox/tab-templates-domains.twig' %} + {% endif %} + {% include 'mailbox/tab-mailboxes.twig' %} + {% if mailcow_cc_role == 'admin' %} + {% include 'mailbox/tab-templates-mbox.twig' %} + {% endif %} + {% include 'mailbox/tab-resources.twig' %} + {% include 'mailbox/tab-domain-aliases.twig' %} + {% include 'mailbox/tab-mbox-aliases.twig' %} + {% include 'mailbox/tab-syncjobs.twig' %} + {% include 'mailbox/tab-filters.twig' %} + {% include 'mailbox/tab-bcc.twig' %} + {% include 'mailbox/tab-tls-policy.twig' %} +
    +
    +
    +
    + +{% include 'modals/mailbox.twig' %} + + +{% endblock %} diff --git a/mailcow/data/web/templates/mailbox/rl-frame.twig b/mailcow/data/web/templates/mailbox/rl-frame.twig new file mode 100644 index 0000000..26af5fa --- /dev/null +++ b/mailcow/data/web/templates/mailbox/rl-frame.twig @@ -0,0 +1,4 @@ + + + + diff --git a/mailcow/data/web/templates/mailbox/tab-bcc.twig b/mailcow/data/web/templates/mailbox/tab-bcc.twig new file mode 100644 index 0000000..eedb04b --- /dev/null +++ b/mailcow/data/web/templates/mailbox/tab-bcc.twig @@ -0,0 +1,104 @@ +
    + + +
    diff --git a/mailcow/data/web/templates/mailbox/tab-domain-aliases.twig b/mailcow/data/web/templates/mailbox/tab-domain-aliases.twig new file mode 100644 index 0000000..4bbfbbe --- /dev/null +++ b/mailcow/data/web/templates/mailbox/tab-domain-aliases.twig @@ -0,0 +1,49 @@ + diff --git a/mailcow/data/web/templates/mailbox/tab-domains.twig b/mailcow/data/web/templates/mailbox/tab-domains.twig new file mode 100644 index 0000000..49cb89b --- /dev/null +++ b/mailcow/data/web/templates/mailbox/tab-domains.twig @@ -0,0 +1,58 @@ +
    +
    +
    + + {{ lang.mailbox.domains }} + +
    + +
    +
    +
    + {#
    #} +
    +
    + + + + {% if mailcow_cc_role == 'admin' %} + + {% endif %} +
    +
    +
    +
    +
    + + + + {% if mailcow_cc_role == 'admin' %} + + {% endif %} +
    +
    +
    +
    +
    diff --git a/mailcow/data/web/templates/mailbox/tab-filters.twig b/mailcow/data/web/templates/mailbox/tab-filters.twig new file mode 100644 index 0000000..8ccff27 --- /dev/null +++ b/mailcow/data/web/templates/mailbox/tab-filters.twig @@ -0,0 +1,96 @@ +
    +
    +
    + + {{ lang.mailbox.filters }} + +
    + +
    +
    + +
    +
    diff --git a/mailcow/data/web/templates/mailbox/tab-mailboxes.twig b/mailcow/data/web/templates/mailbox/tab-mailboxes.twig new file mode 100644 index 0000000..fce7d62 --- /dev/null +++ b/mailcow/data/web/templates/mailbox/tab-mailboxes.twig @@ -0,0 +1,244 @@ +
    +
    +
    + + {{ lang.mailbox.mailboxes }} + +
    + +
    +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    +
    diff --git a/mailcow/data/web/templates/mailbox/tab-mbox-aliases.twig b/mailcow/data/web/templates/mailbox/tab-mbox-aliases.twig new file mode 100644 index 0000000..02b5b6d --- /dev/null +++ b/mailcow/data/web/templates/mailbox/tab-mbox-aliases.twig @@ -0,0 +1,62 @@ + diff --git a/mailcow/data/web/templates/mailbox/tab-resources.twig b/mailcow/data/web/templates/mailbox/tab-resources.twig new file mode 100644 index 0000000..34728ed --- /dev/null +++ b/mailcow/data/web/templates/mailbox/tab-resources.twig @@ -0,0 +1,53 @@ +
    +
    +
    + + {{ lang.mailbox.resources }} + +
    + +
    +
    + +
    +
    diff --git a/mailcow/data/web/templates/mailbox/tab-syncjobs.twig b/mailcow/data/web/templates/mailbox/tab-syncjobs.twig new file mode 100644 index 0000000..bfd8eef --- /dev/null +++ b/mailcow/data/web/templates/mailbox/tab-syncjobs.twig @@ -0,0 +1,53 @@ + diff --git a/mailcow/data/web/templates/mailbox/tab-templates-domains.twig b/mailcow/data/web/templates/mailbox/tab-templates-domains.twig new file mode 100644 index 0000000..f3c6a67 --- /dev/null +++ b/mailcow/data/web/templates/mailbox/tab-templates-domains.twig @@ -0,0 +1,51 @@ +
    +
    +
    + + {{ lang.mailbox.domain_templates }} + +
    + +
    +
    +
    +
    +
    + + + + {% if mailcow_cc_role == 'admin' %} + + {% endif %} +
    +
    +
    +
    +
    + + + + {% if mailcow_cc_role == 'admin' %} + + {% endif %} +
    +
    +
    +
    +
    diff --git a/mailcow/data/web/templates/mailbox/tab-templates-mbox.twig b/mailcow/data/web/templates/mailbox/tab-templates-mbox.twig new file mode 100644 index 0000000..71edb9d --- /dev/null +++ b/mailcow/data/web/templates/mailbox/tab-templates-mbox.twig @@ -0,0 +1,51 @@ +
    +
    +
    + + {{ lang.mailbox.mailbox_templates }} + +
    + +
    +
    +
    +
    +
    + + + + {% if mailcow_cc_role == 'admin' %} + + {% endif %} +
    +
    +
    +
    +
    + + + + {% if mailcow_cc_role == 'admin' %} + + {% endif %} +
    +
    +
    +
    +
    diff --git a/mailcow/data/web/templates/mailbox/tab-tls-policy.twig b/mailcow/data/web/templates/mailbox/tab-tls-policy.twig new file mode 100644 index 0000000..3195b75 --- /dev/null +++ b/mailcow/data/web/templates/mailbox/tab-tls-policy.twig @@ -0,0 +1,50 @@ + diff --git a/mailcow/data/web/templates/modals/admin.twig b/mailcow/data/web/templates/modals/admin.twig new file mode 100644 index 0000000..2271ca7 --- /dev/null +++ b/mailcow/data/web/templates/modals/admin.twig @@ -0,0 +1,249 @@ + + + + + + + + + + + + + + diff --git a/mailcow/data/web/templates/modals/footer.twig b/mailcow/data/web/templates/modals/footer.twig new file mode 100644 index 0000000..8ff112d --- /dev/null +++ b/mailcow/data/web/templates/modals/footer.twig @@ -0,0 +1,365 @@ +{% if mailcow_cc_role %} + + + + + + + +{% endif %} +{% if pending_tfa_methods %} + +{% endif %} +{% if mailcow_cc_role == 'admin' %} + +{% endif %} + + + diff --git a/mailcow/data/web/templates/modals/mailbox.twig b/mailcow/data/web/templates/modals/mailbox.twig new file mode 100644 index 0000000..85091da --- /dev/null +++ b/mailcow/data/web/templates/modals/mailbox.twig @@ -0,0 +1,1283 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mailcow/data/web/templates/modals/quarantine.twig b/mailcow/data/web/templates/modals/quarantine.twig new file mode 100644 index 0000000..3ccefa5 --- /dev/null +++ b/mailcow/data/web/templates/modals/quarantine.twig @@ -0,0 +1,68 @@ + diff --git a/mailcow/data/web/templates/modals/queue.twig b/mailcow/data/web/templates/modals/queue.twig new file mode 100644 index 0000000..68fe912 --- /dev/null +++ b/mailcow/data/web/templates/modals/queue.twig @@ -0,0 +1,14 @@ + + \ No newline at end of file diff --git a/mailcow/data/web/templates/modals/user.twig b/mailcow/data/web/templates/modals/user.twig new file mode 100644 index 0000000..b8e3bf3 --- /dev/null +++ b/mailcow/data/web/templates/modals/user.twig @@ -0,0 +1,379 @@ + + + + + + + + + + + + + + + + diff --git a/mailcow/data/web/templates/oauth/authorize.twig b/mailcow/data/web/templates/oauth/authorize.twig new file mode 100644 index 0000000..4e29dcf --- /dev/null +++ b/mailcow/data/web/templates/oauth/authorize.twig @@ -0,0 +1,27 @@ +{% extends 'base.twig' %} + +{% block content %} +
    +
    {{ lang.oauth2.authorize_app }}
    +
    + {% if mailcow_cc_role == 'user' %} +

    {{ lang.oauth2.scope_ask_permission }}:

    +
    +
    {{ lang.oauth2.profile }}
    +
    {{ lang.oauth2.profile_desc }}
    +
    +
    +
    +
    + + {{ lang.oauth2.deny }} + +
    +
    +
    + {% else %} +

    {{ lang.oauth2.access_denied }}

    + {% endif %} +
    +
    +{% endblock %} diff --git a/mailcow/data/web/templates/qhandler.twig b/mailcow/data/web/templates/qhandler.twig new file mode 100644 index 0000000..b8b8725 --- /dev/null +++ b/mailcow/data/web/templates/qhandler.twig @@ -0,0 +1,83 @@ +{% extends 'base.twig' %} + +{% block content %} +
    +{% if quick_release or quick_delete %} +
    +
    +
    {{ lang.header.quarantine }}
    +
    + + {% if quick_release %} + {{ lang.quarantine.release }} + {% else %} + {{ lang.quarantine.remove }} + {% endif %} + +
    +

    {{ lang.quarantine.qhandler_success }}

    +
    +
    +
    +{% else %} +{% if is_action_release_delete and is_hash_present %} +
    +
    +
    {{ lang.header.quarantine }}
    +
    + {% if action == 'release' %} + {{ lang.quarantine.release }}
    + {% endif %} + {% if action == 'delete' %} + {{ lang.quarantine.remove }}
    + {% endif %} + +
    + +

    {{ lang.quarantine.spam_score }}:

    +

    +
    +
    + +

    +
    +
    + +

    +
    +
    + +

    +
    +
    + +

    +
    +
    + +

    +
    +
    +
    +
    + {% if action == 'release' %} + + {% endif %} + {% if action == 'delete' %} + + {% endif %} +
    +
    +
    +
    +
    +
    +{% endif %} +{% endif %} +
    + + + +{% endblock %} diff --git a/mailcow/data/web/templates/quarantine.twig b/mailcow/data/web/templates/quarantine.twig new file mode 100644 index 0000000..79b5ea1 --- /dev/null +++ b/mailcow/data/web/templates/quarantine.twig @@ -0,0 +1,72 @@ +{% extends 'base.twig' %} + +{% block content %} +
    +
    +
    +
    + {{ lang.quarantine.quarantine }} +
    + +
    +
    +
    + +

    {{ lang.quarantine.qinfo|raw }}

    +

    + {% if not quarantine_settings.retention_size or not quarantine_settings.max_size %} +

    {{ lang.quarantine.disabled_by_config }}
    + {% else %} +

    + {{ lang.quarantine.settings_info|format(quarantine_settings.retention_size, quarantine_settings.max_size)|raw }} +

    + {% endif %} +

    +
    + +
    +
    +
    +
    + +{% include 'modals/quarantine.twig' %} + + +{% endblock %} diff --git a/mailcow/data/web/templates/queue.twig b/mailcow/data/web/templates/queue.twig new file mode 100644 index 0000000..e843c18 --- /dev/null +++ b/mailcow/data/web/templates/queue.twig @@ -0,0 +1,61 @@ +{% extends 'base.twig' %} + +{% block content %} +
    +
    + {{ lang.queue.queue_manager }} +
    + +
    +
    +
    +

    {{ lang.queue.info|raw }}

    +

    {{ lang.queue.legend|raw }}

    +
      +
    • {{ lang.queue.deliver_mail }} | {{ lang.queue.deliver_mail_legend }}
    • +
    • {{ lang.queue.unhold_mail }} | {{ lang.queue.unhold_mail_legend }}
    • +
    • {{ lang.queue.hold_mail }} | {{ lang.queue.hold_mail_legend }}
    • +
    +
    + +
    +
    + +{% include 'modals/queue.twig' %} + + +{% endblock %} diff --git a/mailcow/data/web/templates/reset-password.twig b/mailcow/data/web/templates/reset-password.twig new file mode 100644 index 0000000..5eb2027 --- /dev/null +++ b/mailcow/data/web/templates/reset-password.twig @@ -0,0 +1,57 @@ +{% extends 'base.twig' %} + +{% block navbar %}{% endblock %} + +{% block content %} +
    +
    +
    +
    + {{ lang.login.reset_password }} +
    + + +
    +
    +
    + + {{ ui_texts.main_name|raw }}
    + + {% if is_reset_token_valid %} +
    + + + + + {{ lang.login.password_mismatch }} +
    + +
    +
    + {% elseif reset_token is null %} +
    + + +
    + +
    +
    + {% else %} +

    {{ lang.login.invalid_pass_reset_token|raw }}

    + {{ lang.login.back_to_mailcow }} + {% endif %} + + +
    +
    +
    +
    + + +{% endblock %} diff --git a/mailcow/data/web/templates/tfa_keys.twig b/mailcow/data/web/templates/tfa_keys.twig new file mode 100644 index 0000000..c74571b --- /dev/null +++ b/mailcow/data/web/templates/tfa_keys.twig @@ -0,0 +1,15 @@ +
    + {% if tfa_data.additional %} + {% for key_info in tfa_data.additional %} +
    + +
    + + {{ key_info.key_id }} + [{{ lang.admin.remove }}] + +
    +
    + {% endfor %} + {% endif %} +
    diff --git a/mailcow/data/web/templates/user.twig b/mailcow/data/web/templates/user.twig new file mode 100644 index 0000000..4712fa3 --- /dev/null +++ b/mailcow/data/web/templates/user.twig @@ -0,0 +1,48 @@ +{% extends 'base.twig' %} + +{% block content %} +
    + + + +
    +
    +
    + {% include 'user/tab-user-auth.twig' %} + {% include 'user/tab-user-details.twig' %} + {% include 'user/tab-user-settings.twig' %} + {% if acl.spam_alias == 1 %}{% include 'user/SpamAliases.twig' %}{% endif %} + {% if acl.spam_score == 1 or acl.spam_policy == 1 %}{% include 'user/Spamfilter.twig' %}{% endif %} + {% if acl.syncjobs == 1 %}{% include 'user/Syncjobs.twig' %}{% endif %} + {% if acl.app_passwds == 1 %}{% include 'user/AppPasswds.twig' %}{% endif %} + {% if acl.pushover == 1 %}{% include 'user/Pushover.twig' %}{% endif %} +
    +
    +
    + {% include 'user_domainadmin_common.twig' %} +
    + {% endblock %} diff --git a/mailcow/data/web/templates/user/AppPasswds.twig b/mailcow/data/web/templates/user/AppPasswds.twig new file mode 100644 index 0000000..e5b6f7d --- /dev/null +++ b/mailcow/data/web/templates/user/AppPasswds.twig @@ -0,0 +1,54 @@ + diff --git a/mailcow/data/web/templates/user/Pushover.twig b/mailcow/data/web/templates/user/Pushover.twig new file mode 100644 index 0000000..acc019c --- /dev/null +++ b/mailcow/data/web/templates/user/Pushover.twig @@ -0,0 +1,118 @@ +
    +
    +
    + + Pushover API +
    +
    +
    + + + +
    +
    +

    +
    +
    +

    {{ lang.user.pushover_info|format(mailcow_cc_username)|raw }}

    +

    {{ lang.edit.pushover_vars|raw }}: {SUBJECT}, {SENDER}, {SENDER_ADDRESS}, {SENDER_NAME}, {TO_NAME}, {TO_ADDRESS}, {MSG_ID}

    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + + {{ lang.user.advanced_settings }} + +
    +
    +
    +
    +
    + + +
    + +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    diff --git a/mailcow/data/web/templates/user/SpamAliases.twig b/mailcow/data/web/templates/user/SpamAliases.twig new file mode 100644 index 0000000..83b056c --- /dev/null +++ b/mailcow/data/web/templates/user/SpamAliases.twig @@ -0,0 +1,47 @@ + diff --git a/mailcow/data/web/templates/user/Spamfilter.twig b/mailcow/data/web/templates/user/Spamfilter.twig new file mode 100644 index 0000000..1f69df9 --- /dev/null +++ b/mailcow/data/web/templates/user/Spamfilter.twig @@ -0,0 +1,89 @@ +
    +
    +
    + + {{ lang.user.spamfilter }} +
    +
    +

    {{ lang.user.spamfilter_behavior }}

    +
    +
    +
    +
    +
    +
    + +
      +
    • {{ lang.user.spamfilter_green }}
    • +
    • {{ lang.user.spamfilter_yellow }}
    • +
    • {{ lang.user.spamfilter_red }}
    • +
    +
    +
    + +
    +
    +
    +
    +
    +
    +

    {{ lang.user.spamfilter_wl }}

    +

    {{ lang.user.spamfilter_wl_desc|raw }}

    +
    +
    + + +
    +
    +
    + +
    +
    +

    {{ lang.user.spamfilter_bl }}

    +

    {{ lang.user.spamfilter_bl_desc|raw }}

    +
    +
    + + +
    +
    +
    + +
    +
    +
    +
    +
    diff --git a/mailcow/data/web/templates/user/Syncjobs.twig b/mailcow/data/web/templates/user/Syncjobs.twig new file mode 100644 index 0000000..1267b4f --- /dev/null +++ b/mailcow/data/web/templates/user/Syncjobs.twig @@ -0,0 +1,53 @@ + diff --git a/mailcow/data/web/templates/user/tab-user-auth.twig b/mailcow/data/web/templates/user/tab-user-auth.twig new file mode 100644 index 0000000..a7e0254 --- /dev/null +++ b/mailcow/data/web/templates/user/tab-user-auth.twig @@ -0,0 +1,215 @@ +
    +
    +
    + + {{ lang.user.mailbox_general }} +
    +
    + {% if mailboxdata.attributes.force_pw_update == '1' %} +
    {{ lang.user.force_pw_update|raw }}
    + {% endif %} +
    +
    + {% if not skip_sogo %} +
    +
    + {% if dual_login and allow_admin_email_login == 'n' or mailboxdata.attributes.force_pw_update == '1' %} + + {% elseif dual_login %} + + {{ lang.user.open_webmail_sso }} + + {% else %} + + {{ lang.user.open_webmail_sso }} + + {% endif %} +
    +
    + {% endif %} + + {{ lang.user.overview }} +
    +
    +
    {{ mailboxdata.messages }} {{ lang.user.messages }}
    +
    {{ mailboxdata.quota_used|formatBytes(2) }} / {% if mailboxdata.quota == 0 %}∞{% else %}{{ mailboxdata.quota|formatBytes(2) }}{% endif %}
    +
    +
    +
    + {{ mailboxdata.percent_in_use }}% +
    +
    + +
    +
    + {{ lang.user.protocols }}: +
    +
    + +
    + {% if mailboxdata.attributes.imap_access == 1 %}
    IMAP
    {% else %}
    IMAP
    {% endif %} + {% if mailboxdata.attributes.smtp_access == 1 %}
    SMTP
    {% else %}
    SMTP
    {% endif %} + {% if mailboxdata.attributes.sieve_access == 1 %}
    Sieve
    {% else %}
    Sieve
    {% endif %} + {% if mailboxdata.attributes.pop3_access == 1 %}
    POP3
    {% else %}
    POP3
    {% endif %} +
    +
    +
    +
    +
    + {{ lang.user.apple_connection_profile }}: +
    +
    + + {% if not skip_sogo %} + + {% endif %} +
    +
    +
    +
    + {{ lang.user.apple_connection_profile }}
    {{ lang.user.with_app_password }}:
    +
    +
    + + {% if not skip_sogo %} + + {% endif %} +
    +
    + + + {% if mailboxdata.authsource == "mailcow" %} + {{ lang.user.authentication }} +
    + {# Password Change #} +
    +
    +
    + {{ lang.user.change_password }} + {% if acl.pw_reset == 1 %} + {{ lang.user.pw_recovery_email }}

    + {% endif %} +
    +
    + {% endif %} + {# TFA #} + {% if mailboxdata.authsource == "mailcow" or mailboxdata.authsource == "ldap" %} +
    +
    + {{ lang.tfa.tfa }}: +
    +
    + {{ tfa_data.pretty }} + {% include 'tfa_keys.twig' %} +
    +
    +
    +
    + {{ lang.tfa.set_tfa }}: +
    +
    + + +
    +
    + {% endif %} + {# FIDO2 #} + {% if mailboxdata.authsource == "mailcow" %} +
    +
    +

    {{ lang.fido2.fido2_auth }}

    +
    +
    +
    +
    + {{ lang.fido2.known_ids }}: +
    +
    +
    + + + + + + {% include 'fido2.twig' %} +
    ID{{ lang.admin.action }}
    +
    +
    +
    +
    +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    {{ lang.fido2.register_status }}:
    +
    +
    -
    +
    +
    +
    + {% endif %} +
    +
    + + {{ lang.user.recent_successful_connections }} +
    + Loading... +
    +
    +
    + +
    + + {{ lang.user.clear_recent_successful_connections }} + + +
    + +
    +
    +
    +
    +
    diff --git a/mailcow/data/web/templates/user/tab-user-details.twig b/mailcow/data/web/templates/user/tab-user-details.twig new file mode 100644 index 0000000..62842fc --- /dev/null +++ b/mailcow/data/web/templates/user/tab-user-details.twig @@ -0,0 +1,72 @@ +
    +
    +
    + + {{ lang.user.mailbox_details }} +
    +
    +
    +
    {{ lang.user.direct_aliases }}: +

    {{ lang.user.direct_aliases_desc }}

    +
    +
    + {% for direct_alias, direct_alias_meta in user_get_alias_details.direct_aliases %} + {% if direct_alias_meta.public_comment %} + {{ direct_alias }} — {{ direct_alias_meta.public_comment }}
    + {% else %} + {{ direct_alias }}
    + {% endif %} + {% else %} + + {% endfor %} +
    +
    +
    +
    +
    {{ lang.user.shared_aliases }}: +

    {{ lang.user.shared_aliases_desc }}

    +
    +
    + {% for shared_alias, shared_alias_meta in user_get_alias_details.shared_aliases %} + {% if shared_alias_meta.public_comment %} + {{ shared_alias }} — {{ shared_alias_meta.public_comment }}
    + {% else %} + {{ shared_alias }}
    + {% endif %} + {% else %} + + {% endfor %} +
    +
    +
    +
    +
    {{ lang.user.aliases_also_send_as }}:
    +
    +

    + {% if user_get_alias_details.aliases_also_send_as == '*' %} + {{ lang.user.sender_acl_disabled | raw }} + {% elseif user_get_alias_details.aliases_also_send_as %} + {{ user_get_alias_details.aliases_also_send_as }} + {% else %} + + {% endif %} +

    +
    +
    +
    +
    {{ lang.user.aliases_send_as_all }}:
    +
    +

    {% if not user_get_alias_details.aliases_send_as_all %}{% else %}{{ user_get_alias_details.aliases_send_as_all }}{% endif %}

    +
    +
    +
    +
    {{ lang.user.is_catch_all }}:
    +
    +

    {% if not user_get_alias_details.is_catch_all %}{% else %}{{ user_get_alias_details.is_catch_all }}{% endif %}

    +
    +
    +
    +
    +
    diff --git a/mailcow/data/web/templates/user/tab-user-settings.twig b/mailcow/data/web/templates/user/tab-user-settings.twig new file mode 100644 index 0000000..3fe0db3 --- /dev/null +++ b/mailcow/data/web/templates/user/tab-user-settings.twig @@ -0,0 +1,137 @@ +
    +
    +
    + + {{ lang.user.mailbox_settings }} +
    +
    + {# Show tagging options #} +
    +
    {{ lang.user.tag_handling }}:
    +
    +
    + + + +
    +

    {{ lang.user.tag_help_explain|raw }}

    +

    {{ lang.user.tag_help_example|raw }}

    +
    +
    + {# Show TLS policy options #} +
    +
    {{ lang.user.tls_policy }}:
    +
    +
    + + +
    +

    {{ lang.user.tls_policy_warning|raw }}

    +
    +
    + {# Show quarantine_notification options #} +
    +
    {{ lang.user.quarantine_notification }}:
    +
    +
    + + + + +
    +

    {{ lang.user.quarantine_notification_info }}

    +
    +
    +
    +
    {{ lang.user.quarantine_category }}:
    +
    +
    + + + +
    +

    {{ lang.user.quarantine_category_info }}

    +
    +
    + {% if not skip_sogo %} +
    +
    +
    {{ lang.user.eas_reset }}:
    +
    + +

    {{ lang.user.eas_reset_help|raw }}

    +
    +
    +
    +
    {{ lang.user.sogo_profile_reset }}:
    +
    + +

    {{ lang.user.sogo_profile_reset_help|raw }}

    +
    +
    + {% endif %} +
    +
    +
    diff --git a/mailcow/data/web/templates/user_domainadmin_common.twig b/mailcow/data/web/templates/user_domainadmin_common.twig new file mode 100644 index 0000000..64a2205 --- /dev/null +++ b/mailcow/data/web/templates/user_domainadmin_common.twig @@ -0,0 +1,11 @@ +{% include 'modals/user.twig' %} + + diff --git a/mailcow/data/web/templates/user_index.twig b/mailcow/data/web/templates/user_index.twig new file mode 100644 index 0000000..4ed9421 --- /dev/null +++ b/mailcow/data/web/templates/user_index.twig @@ -0,0 +1,120 @@ +{% extends 'base.twig' %} + +{% block navbar %}{% endblock %} + +{% block content %} +
    +
    +
    +
    + {{ lang.login.login_user }} +
    + + +
    +
    +
    + + {% if ui_texts.ui_announcement_text and ui_texts.ui_announcement_active %} +
    {{ ui_texts.ui_announcement_text|rot13 }}
    + {% endif %} + {% if oauth2_request %}{{ lang.oauth2.authorize_app }}{% else %}{{ ui_texts.main_name|raw }}{% endif %}
    + {% if is_mobileconfig %} +
    {{ lang.login.mobileconfig_info }}
    + {% endif %} +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    + + {% if not oauth2_request %} +
    + + +
    + {% endif %} +
    +
    + +
    {{ lang.login.other_logins }}
    +
    + {% if has_iam_sso %} + {{ lang.admin.iam_sso }} + {% endif %} + {{ lang.login.fido2_webauthn }} +
    + {% if login_delay %} +

    {{ lang.login.delayed|format(login_delay) }}

    + {% endif %} +
    + {% if not oauth2_request and (mailcow_apps or app_links) and not hide_mailcow_apps %} + {{ ui_texts.apps_name|raw }}
    +
    + {% for app in mailcow_apps %} + {% if not app.hide %} + {% if not skip_sogo or not is_uri('SOGo', app.link) %} + + {% endif %} + {% endif %} + {% endfor %} + {% for row in app_links %} + {% for key, val in row %} + {% if not val.hide %} +
    + {{ key }} +
    + {% endif %} + {% endfor %} + {% endfor %} +
    + {% endif %} +
    +
    +
    +
    +{% if not oauth2_request and ui_texts.help_text %} +
    +
    +
    + +
    +
    +

    {{ ui_texts.help_text|raw }}

    +
    +
    +
    +
    +
    +{% endif %} +{% endblock %} diff --git a/mailcow/data/web/user.php b/mailcow/data/web/user.php new file mode 100644 index 0000000..7c34ba9 --- /dev/null +++ b/mailcow/data/web/user.php @@ -0,0 +1,93 @@ + "get_friendly_names")); + + $clientconfigstr = "host=" . urlencode($mailcow_hostname) . "&email=" . urlencode($username) . "&name=" . urlencode($mailboxdata['name']) . "&ui=" . urlencode(strtok($_SERVER['HTTP_HOST'], ':')) . "&port=" . urlencode($autodiscover_config['caldav']['port']); + if ($autodiscover_config['useEASforOutlook'] == 'yes') + $clientconfigstr .= "&outlookEAS=1"; + if (file_exists('thunderbird-plugins/version.csv')) { + $fh = fopen('thunderbird-plugins/version.csv', 'r'); + if ($fh) { + while (($row = fgetcsv($fh, 1000, ';')) !== FALSE) { + if ($row[0] == 'sogo-connector@inverse.ca') { + $clientconfigstr .= "&connector=" . urlencode($row[1]); + } + } + fclose($fh); + } + } + + // Get user information about aliases + $user_get_alias_details = user_get_alias_details($username); + $user_get_alias_details['direct_aliases'] = array_filter($user_get_alias_details['direct_aliases']); + $user_get_alias_details['shared_aliases'] = array_filter($user_get_alias_details['shared_aliases']); + $user_domains[] = mailbox('get', 'mailbox_details', $username)['domain']; + $user_alias_domains = $user_get_alias_details['alias_domains']; + if (!empty($user_alias_domains)) { + $user_domains = array_merge($user_domains, $user_alias_domains); + } + + // get number of app passwords + $number_of_app_passwords = 0; + foreach (app_passwd("get") as $app_password) + { + $app_password = app_passwd("details", $app_password['id']); + if ($app_password['active']) + { + $number_of_app_passwords++; + } + } + + $template = 'user.twig'; + $template_data = [ + 'acl' => $_SESSION['acl'], + 'acl_json' => json_encode($_SESSION['acl']), + 'user_spam_score' => mailbox('get', 'spam_score', $username), + 'tfa_data' => $tfa_data, + 'tfa_id' => @$_SESSION['tfa_id'], + 'fido2_data' => $fido2_data, + 'mailboxdata' => $mailboxdata, + 'clientconfigstr' => $clientconfigstr, + 'user_get_alias_details' => $user_get_alias_details, + 'get_tagging_options' => mailbox('get', 'delimiter_action', $username), + 'get_tls_policy' => mailbox('get', 'tls_policy', $username), + 'quarantine_notification' => mailbox('get', 'quarantine_notification', $username), + 'quarantine_category' => mailbox('get', 'quarantine_category', $username), + 'user_domains' => $user_domains, + 'pushover_data' => $pushover_data, + 'lang_user' => json_encode($lang['user']), + 'number_of_app_passwords' => $number_of_app_passwords, + 'lang_datatables' => json_encode($lang['datatables']), + ]; +} +elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { + header('Location: /admin/dashboard'); + exit(); +} +elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { + header('Location: /domainadmin/mailbox'); + exit(); +} +else { + header('Location: /'); + exit(); +} + +$js_minifier->add('/web/js/site/user.js'); +$js_minifier->add('/web/js/site/pwgen.js'); + +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php'; diff --git a/mailcow/docker-compose.yml b/mailcow/docker-compose.yml new file mode 100644 index 0000000..2d68b80 --- /dev/null +++ b/mailcow/docker-compose.yml @@ -0,0 +1,682 @@ +services: + + unbound-mailcow: + image: ghcr.io/mailcow/unbound:1.24 + environment: + - TZ=${TZ} + - SKIP_UNBOUND_HEALTHCHECK=${SKIP_UNBOUND_HEALTHCHECK:-n} + volumes: + - ./data/hooks/unbound:/hooks:Z + - ./data/conf/unbound/unbound.conf:/etc/unbound/unbound.conf:ro,Z + restart: always + tty: true + networks: + mailcow-network: + ipv4_address: ${IPV4_NETWORK:-172.22.1}.254 + aliases: + - unbound + + mysql-mailcow: + image: mariadb:10.11 + depends_on: + - unbound-mailcow + - netfilter-mailcow + stop_grace_period: 45s + volumes: + - mysql-vol-1:/var/lib/mysql/ + - mysql-socket-vol-1:/var/run/mysqld/ + - ./data/conf/mysql/:/etc/mysql/conf.d/:ro,Z + environment: + - TZ=${TZ} + - MYSQL_ROOT_PASSWORD=${DBROOT} + - MYSQL_DATABASE=${DBNAME} + - MYSQL_USER=${DBUSER} + - MYSQL_PASSWORD=${DBPASS} + - MYSQL_INITDB_SKIP_TZINFO=1 + restart: always + ports: + - "${SQL_PORT:-127.0.0.1:13306}:3306" + networks: + mailcow-network: + aliases: + - mysql + + redis-mailcow: + image: redis:7.4.2-alpine + entrypoint: ["/bin/sh","/redis-conf.sh"] + volumes: + - redis-vol-1:/data/ + - ./data/conf/redis/redis-conf.sh:/redis-conf.sh:z + restart: always + depends_on: + - netfilter-mailcow + ports: + - "${REDIS_PORT:-127.0.0.1:7654}:6379" + environment: + - TZ=${TZ} + - REDISPASS=${REDISPASS} + - REDISMASTERPASS=${REDISMASTERPASS:-} + sysctls: + - net.core.somaxconn=4096 + networks: + mailcow-network: + ipv4_address: ${IPV4_NETWORK:-172.22.1}.249 + aliases: + - redis + + clamd-mailcow: + image: ghcr.io/mailcow/clamd:1.70 + restart: always + depends_on: + unbound-mailcow: + condition: service_healthy + dns: + - ${IPV4_NETWORK:-172.22.1}.254 + environment: + - TZ=${TZ} + - SKIP_CLAMD=${SKIP_CLAMD:-n} + volumes: + - ./data/conf/clamav/:/etc/clamav/:Z + - clamd-db-vol-1:/var/lib/clamav + networks: + mailcow-network: + aliases: + - clamd + + rspamd-mailcow: + image: ghcr.io/mailcow/rspamd:2.1 + stop_grace_period: 30s + depends_on: + - dovecot-mailcow + - clamd-mailcow + environment: + - TZ=${TZ} + - IPV4_NETWORK=${IPV4_NETWORK:-172.22.1} + - IPV6_NETWORK=${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64} + - REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-} + - REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-} + - REDISPASS=${REDISPASS} + - SPAMHAUS_DQS_KEY=${SPAMHAUS_DQS_KEY:-} + volumes: + - ./data/hooks/rspamd:/hooks:Z + - ./data/conf/rspamd/custom/:/etc/rspamd/custom:z + - ./data/conf/rspamd/override.d/:/etc/rspamd/override.d:Z + - ./data/conf/rspamd/local.d/:/etc/rspamd/local.d:Z + - ./data/conf/rspamd/plugins.d/:/etc/rspamd/plugins.d:Z + - ./data/conf/rspamd/lua/:/etc/rspamd/lua/:ro,Z + - ./data/conf/rspamd/rspamd.conf.local:/etc/rspamd/rspamd.conf.local:Z + - ./data/conf/rspamd/rspamd.conf.override:/etc/rspamd/rspamd.conf.override:Z + - rspamd-vol-1:/var/lib/rspamd + restart: always + hostname: rspamd + dns: + - ${IPV4_NETWORK:-172.22.1}.254 + networks: + mailcow-network: + aliases: + - rspamd + + php-fpm-mailcow: + image: ghcr.io/mailcow/phpfpm:1.93 + command: "php-fpm -d date.timezone=${TZ} -d expose_php=0" + depends_on: + - redis-mailcow + volumes: + - ./data/hooks/phpfpm:/hooks:Z + - ./data/web:/web:z + - ./data/conf/rspamd/dynmaps:/dynmaps:ro,z + - ./data/conf/rspamd/custom/:/rspamd_custom_maps:z + - ./data/conf/dovecot/auth/mailcowauth.php:/mailcowauth/mailcowauth.php:z + - ./data/web/inc/functions.inc.php:/mailcowauth/functions.inc.php:z + - ./data/web/inc/functions.auth.inc.php:/mailcowauth/functions.auth.inc.php:z + - ./data/web/inc/sessions.inc.php:/mailcowauth/sessions.inc.php:z + - ./data/web/inc/functions.mailbox.inc.php:/mailcowauth/functions.mailbox.inc.php:z + - ./data/web/inc/functions.ratelimit.inc.php:/mailcowauth/functions.ratelimit.inc.php:z + - ./data/web/inc/functions.acl.inc.php:/mailcowauth/functions.acl.inc.php:z + - rspamd-vol-1:/var/lib/rspamd + - mysql-socket-vol-1:/var/run/mysqld/ + - ./data/conf/sogo/:/etc/sogo/:z + - ./data/conf/rspamd/meta_exporter:/meta_exporter:ro,z + - ./data/conf/phpfpm/crons:/crons:z + - ./data/conf/phpfpm/sogo-sso/:/etc/sogo-sso/:z + - ./data/conf/phpfpm/php-fpm.d/pools.conf:/usr/local/etc/php-fpm.d/z-pools.conf:Z + - ./data/conf/phpfpm/php-conf.d/opcache-recommended.ini:/usr/local/etc/php/conf.d/opcache-recommended.ini:Z + - ./data/conf/phpfpm/php-conf.d/upload.ini:/usr/local/etc/php/conf.d/upload.ini:Z + - ./data/conf/phpfpm/php-conf.d/other.ini:/usr/local/etc/php/conf.d/zzz-other.ini:Z + - ./data/conf/dovecot/global_sieve_before:/global_sieve/before:z + - ./data/conf/dovecot/global_sieve_after:/global_sieve/after:z + - ./data/assets/templates:/tpls:z + - ./data/conf/nginx/:/etc/nginx/conf.d/:z + dns: + - ${IPV4_NETWORK:-172.22.1}.254 + environment: + - REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-} + - REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-} + - REDISPASS=${REDISPASS} + - LOG_LINES=${LOG_LINES:-9999} + - TZ=${TZ} + - DBNAME=${DBNAME} + - DBUSER=${DBUSER} + - DBPASS=${DBPASS} + - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} + - MAILCOW_PASS_SCHEME=${MAILCOW_PASS_SCHEME:-BLF-CRYPT} + - IMAP_PORT=${IMAP_PORT:-143} + - IMAPS_PORT=${IMAPS_PORT:-993} + - POP_PORT=${POP_PORT:-110} + - POPS_PORT=${POPS_PORT:-995} + - SIEVE_PORT=${SIEVE_PORT:-4190} + - IPV4_NETWORK=${IPV4_NETWORK:-172.22.1} + - IPV6_NETWORK=${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64} + - SUBMISSION_PORT=${SUBMISSION_PORT:-587} + - SMTPS_PORT=${SMTPS_PORT:-465} + - SMTP_PORT=${SMTP_PORT:-25} + - API_KEY=${API_KEY:-invalid} + - API_KEY_READ_ONLY=${API_KEY_READ_ONLY:-invalid} + - API_ALLOW_FROM=${API_ALLOW_FROM:-invalid} + - COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME:-mailcow-dockerized} + - SKIP_FTS=${SKIP_FTS:-y} + - SKIP_CLAMD=${SKIP_CLAMD:-n} + - SKIP_SOGO=${SKIP_SOGO:-n} + - ALLOW_ADMIN_EMAIL_LOGIN=${ALLOW_ADMIN_EMAIL_LOGIN:-n} + - MASTER=${MASTER:-y} + - DEV_MODE=${DEV_MODE:-n} + - DEMO_MODE=${DEMO_MODE:-n} + - WEBAUTHN_ONLY_TRUSTED_VENDORS=${WEBAUTHN_ONLY_TRUSTED_VENDORS:-n} + - CLUSTERMODE=${CLUSTERMODE:-} + - ADDITIONAL_SERVER_NAMES=${ADDITIONAL_SERVER_NAMES:-} + restart: always + labels: + ofelia.enabled: "true" + ofelia.job-exec.phpfpm_keycloak_sync.schedule: "@every 1m" + ofelia.job-exec.phpfpm_keycloak_sync.no-overlap: "true" + ofelia.job-exec.phpfpm_keycloak_sync.command: "/bin/bash -c \"php /crons/keycloak-sync.php || exit 0\"" + ofelia.job-exec.phpfpm_ldap_sync.schedule: "@every 1m" + ofelia.job-exec.phpfpm_ldap_sync.no-overlap: "true" + ofelia.job-exec.phpfpm_ldap_sync.command: "/bin/bash -c \"php /crons/ldap-sync.php || exit 0\"" + networks: + mailcow-network: + aliases: + - phpfpm + + sogo-mailcow: + image: ghcr.io/mailcow/sogo:1.131 + environment: + - DBNAME=${DBNAME} + - DBUSER=${DBUSER} + - DBPASS=${DBPASS} + - TZ=${TZ} + - LOG_LINES=${LOG_LINES:-9999} + - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} + - MAILCOW_PASS_SCHEME=${MAILCOW_PASS_SCHEME:-BLF-CRYPT} + - ACL_ANYONE=${ACL_ANYONE:-disallow} + - ALLOW_ADMIN_EMAIL_LOGIN=${ALLOW_ADMIN_EMAIL_LOGIN:-n} + - IPV4_NETWORK=${IPV4_NETWORK:-172.22.1} + - SOGO_EXPIRE_SESSION=${SOGO_EXPIRE_SESSION:-480} + - SKIP_SOGO=${SKIP_SOGO:-n} + - MASTER=${MASTER:-y} + - REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-} + - REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-} + - REDISPASS=${REDISPASS} + dns: + - ${IPV4_NETWORK:-172.22.1}.254 + volumes: + - ./data/hooks/sogo:/hooks:Z + - ./data/conf/sogo/:/etc/sogo/:z + - ./data/web/inc/init_db.inc.php:/init_db.inc.php:z + - ./data/conf/sogo/custom-favicon.ico:/usr/lib/GNUstep/SOGo/WebServerResources/img/sogo.ico:z + - ./data/conf/sogo/custom-shortlogo.svg:/usr/lib/GNUstep/SOGo/WebServerResources/img/sogo-compact.svg:z + - ./data/conf/sogo/custom-fulllogo.svg:/usr/lib/GNUstep/SOGo/WebServerResources/img/sogo-full.svg:z + - ./data/conf/sogo/custom-fulllogo.png:/usr/lib/GNUstep/SOGo/WebServerResources/img/sogo-logo.png:z + - ./data/conf/sogo/custom-theme.js:/usr/lib/GNUstep/SOGo/WebServerResources/js/theme.js:z + - ./data/conf/sogo/custom-sogo.js:/usr/lib/GNUstep/SOGo/WebServerResources/js/custom-sogo.js:z + - mysql-socket-vol-1:/var/run/mysqld/ + - sogo-web-vol-1:/sogo_web + - sogo-userdata-backup-vol-1:/sogo_backup + labels: + ofelia.enabled: "true" + ofelia.job-exec.sogo_sessions.schedule: "@every 1m" + ofelia.job-exec.sogo_sessions.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-tool -v expire-sessions $${SOGO_EXPIRE_SESSION} || exit 0\"" + ofelia.job-exec.sogo_ealarms.schedule: "@every 1m" + ofelia.job-exec.sogo_ealarms.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-ealarms-notify -p /etc/sogo/cron.creds || exit 0\"" + ofelia.job-exec.sogo_eautoreply.schedule: "@every 5m" + ofelia.job-exec.sogo_eautoreply.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-tool update-autoreply -p /etc/sogo/cron.creds || exit 0\"" + ofelia.job-exec.sogo_backup.schedule: "@every 24h" + ofelia.job-exec.sogo_backup.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-tool backup /sogo_backup ALL || exit 0\"" + restart: always + networks: + mailcow-network: + ipv4_address: ${IPV4_NETWORK:-172.22.1}.248 + aliases: + - sogo + + dovecot-mailcow: + image: ghcr.io/mailcow/dovecot:2.33 + depends_on: + - mysql-mailcow + - netfilter-mailcow + - redis-mailcow + dns: + - ${IPV4_NETWORK:-172.22.1}.254 + cap_add: + - NET_BIND_SERVICE + volumes: + - ./data/hooks/dovecot:/hooks:Z + - ./data/conf/dovecot:/etc/dovecot:z + - ./data/assets/ssl:/etc/ssl/mail/:ro,z + - ./data/conf/sogo/:/etc/sogo/:z + - ./data/conf/phpfpm/sogo-sso/:/etc/phpfpm/:z + - vmail-vol-1:/var/vmail + - vmail-index-vol-1:/var/vmail_index + - crypt-vol-1:/mail_crypt/ + - ./data/conf/rspamd/custom/:/etc/rspamd/custom:z + - ./data/assets/templates:/templates:z + - rspamd-vol-1:/var/lib/rspamd + - mysql-socket-vol-1:/var/run/mysqld/ + environment: + - DOVECOT_MASTER_USER=${DOVECOT_MASTER_USER:-} + - DOVECOT_MASTER_PASS=${DOVECOT_MASTER_PASS:-} + - MAILCOW_REPLICA_IP=${MAILCOW_REPLICA_IP:-} + - DOVEADM_REPLICA_PORT=${DOVEADM_REPLICA_PORT:-} + - LOG_LINES=${LOG_LINES:-9999} + - DBNAME=${DBNAME} + - DBUSER=${DBUSER} + - DBPASS=${DBPASS} + - TZ=${TZ} + - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} + - MAILCOW_PASS_SCHEME=${MAILCOW_PASS_SCHEME:-BLF-CRYPT} + - IPV4_NETWORK=${IPV4_NETWORK:-172.22.1} + - ALLOW_ADMIN_EMAIL_LOGIN=${ALLOW_ADMIN_EMAIL_LOGIN:-n} + - MAILDIR_GC_TIME=${MAILDIR_GC_TIME:-7200} + - ACL_ANYONE=${ACL_ANYONE:-disallow} + - SKIP_FTS=${SKIP_FTS:-y} + - FTS_HEAP=${FTS_HEAP:-512} + - FTS_PROCS=${FTS_PROCS:-3} + - MAILDIR_SUB=${MAILDIR_SUB:-} + - MASTER=${MASTER:-y} + - REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-} + - REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-} + - REDISPASS=${REDISPASS} + - COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME:-mailcow-dockerized} + ports: + - "${DOVEADM_PORT:-127.0.0.1:19991}:12345" + - "${IMAP_PORT:-143}:143" + - "${IMAPS_PORT:-993}:993" + - "${POP_PORT:-110}:110" + - "${POPS_PORT:-995}:995" + - "${SIEVE_PORT:-4190}:4190" + restart: always + tty: true + labels: + ofelia.enabled: "true" + ofelia.job-exec.dovecot_imapsync_runner.schedule: "@every 1m" + ofelia.job-exec.dovecot_imapsync_runner.no-overlap: "true" + ofelia.job-exec.dovecot_imapsync_runner.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu nobody /usr/local/bin/imapsync_runner.pl || exit 0\"" + ofelia.job-exec.dovecot_trim_logs.schedule: "@every 1m" + ofelia.job-exec.dovecot_trim_logs.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu vmail /usr/local/bin/trim_logs.sh || exit 0\"" + ofelia.job-exec.dovecot_quarantine.schedule: "@every 20m" + ofelia.job-exec.dovecot_quarantine.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu vmail /usr/local/bin/quarantine_notify.py || exit 0\"" + ofelia.job-exec.dovecot_clean_q_aged.schedule: "@every 24h" + ofelia.job-exec.dovecot_clean_q_aged.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu vmail /usr/local/bin/clean_q_aged.sh || exit 0\"" + ofelia.job-exec.dovecot_maildir_gc.schedule: "@every 30m" + ofelia.job-exec.dovecot_maildir_gc.command: "/bin/bash -c \"source /source_env.sh ; /usr/local/bin/gosu vmail /usr/local/bin/maildir_gc.sh\"" + ofelia.job-exec.dovecot_sarules.schedule: "@every 24h" + ofelia.job-exec.dovecot_sarules.command: "/bin/bash -c \"/usr/local/bin/sa-rules.sh\"" + ofelia.job-exec.dovecot_fts.schedule: "@every 24h" + ofelia.job-exec.dovecot_fts.command: "/bin/bash -c \"/usr/local/bin/gosu vmail /usr/local/bin/optimize-fts.sh\"" + ofelia.job-exec.dovecot_repl_health.schedule: "@every 5m" + ofelia.job-exec.dovecot_repl_health.command: "/bin/bash -c \"/usr/local/bin/gosu vmail /usr/local/bin/repl_health.sh\"" + ulimits: + nproc: 65535 + nofile: + soft: 20000 + hard: 40000 + networks: + mailcow-network: + ipv4_address: ${IPV4_NETWORK:-172.22.1}.250 + aliases: + - dovecot + + postfix-mailcow: + image: ghcr.io/mailcow/postfix:1.80 + depends_on: + mysql-mailcow: + condition: service_started + unbound-mailcow: + condition: service_healthy + volumes: + - ./data/hooks/postfix:/hooks:Z + - ./data/conf/postfix:/opt/postfix/conf:z + - ./data/assets/ssl:/etc/ssl/mail/:ro,z + - postfix-vol-1:/var/spool/postfix + - crypt-vol-1:/var/lib/zeyple + - rspamd-vol-1:/var/lib/rspamd + - mysql-socket-vol-1:/var/run/mysqld/ + environment: + - LOG_LINES=${LOG_LINES:-9999} + - TZ=${TZ} + - DBNAME=${DBNAME} + - DBUSER=${DBUSER} + - DBPASS=${DBPASS} + - REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-} + - REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-} + - REDISPASS=${REDISPASS} + - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} + - SPAMHAUS_DQS_KEY=${SPAMHAUS_DQS_KEY:-} + cap_add: + - NET_BIND_SERVICE + ports: + - "${SMTP_PORT:-25}:25" + - "${SMTPS_PORT:-465}:465" + - "${SUBMISSION_PORT:-587}:587" + restart: always + dns: + - ${IPV4_NETWORK:-172.22.1}.254 + networks: + mailcow-network: + ipv4_address: ${IPV4_NETWORK:-172.22.1}.253 + aliases: + - postfix + + memcached-mailcow: + image: memcached:alpine + restart: always + environment: + - TZ=${TZ} + networks: + mailcow-network: + aliases: + - memcached + + nginx-mailcow: + depends_on: + - redis-mailcow + - php-fpm-mailcow + - sogo-mailcow + - rspamd-mailcow + image: ghcr.io/mailcow/nginx:1.03 + dns: + - ${IPV4_NETWORK:-172.22.1}.254 + environment: + - HTTPS_PORT=${HTTPS_PORT:-443} + - HTTP_PORT=${HTTP_PORT:-80} + - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} + - ADDITIONAL_SERVER_NAMES=${ADDITIONAL_SERVER_NAMES:-} + - TZ=${TZ} + - SKIP_SOGO=${SKIP_SOGO:-n} + - SKIP_RSPAMD=${SKIP_RSPAMD:-n} + - DISABLE_IPv6=${DISABLE_IPv6:-n} + - HTTP_REDIRECT=${HTTP_REDIRECT:-n} + - PHPFPMHOST=${PHPFPMHOST:-} + - SOGOHOST=${SOGOHOST:-} + - RSPAMDHOST=${RSPAMDHOST:-} + - REDISHOST=${REDISHOST:-} + - IPV4_NETWORK=${IPV4_NETWORK:-172.22.1} + - NGINX_USE_PROXY_PROTOCOL=${NGINX_USE_PROXY_PROTOCOL:-n} + - TRUSTED_PROXIES=${TRUSTED_PROXIES:-} + volumes: + - ./data/web:/web:ro,z + - ./data/conf/rspamd/dynmaps:/dynmaps:ro,z + - ./data/assets/ssl/:/etc/ssl/mail/:ro,z + - ./data/conf/nginx/:/etc/nginx/conf.d/:z + - ./data/conf/rspamd/meta_exporter:/meta_exporter:ro,z + - ./data/conf/dovecot/auth/mailcowauth.php:/mailcowauth/mailcowauth.php:z + - ./data/web/inc/functions.inc.php:/mailcowauth/functions.inc.php:z + - ./data/web/inc/functions.auth.inc.php:/mailcowauth/functions.auth.inc.php:z + - ./data/web/inc/sessions.inc.php:/mailcowauth/sessions.inc.php:z + - sogo-web-vol-1:/usr/lib/GNUstep/SOGo/ + ports: + - "${HTTPS_BIND:-}:${HTTPS_PORT:-443}:${HTTPS_PORT:-443}" + - "${HTTP_BIND:-}:${HTTP_PORT:-80}:${HTTP_PORT:-80}" + restart: always + networks: + mailcow-network: + aliases: + - nginx + + acme-mailcow: + depends_on: + nginx-mailcow: + condition: service_started + unbound-mailcow: + condition: service_healthy + image: ghcr.io/mailcow/acme:1.92 + dns: + - ${IPV4_NETWORK:-172.22.1}.254 + environment: + - LOG_LINES=${LOG_LINES:-9999} + - ACME_CONTACT=${ACME_CONTACT:-} + - ADDITIONAL_SAN=${ADDITIONAL_SAN} + - AUTODISCOVER_SAN=${AUTODISCOVER_SAN:-y} + - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} + - DBNAME=${DBNAME} + - DBUSER=${DBUSER} + - DBPASS=${DBPASS} + - SKIP_LETS_ENCRYPT=${SKIP_LETS_ENCRYPT:-n} + - COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME:-mailcow-dockerized} + - DIRECTORY_URL=${DIRECTORY_URL:-} + - ENABLE_SSL_SNI=${ENABLE_SSL_SNI:-n} + - SKIP_IP_CHECK=${SKIP_IP_CHECK:-n} + - SKIP_HTTP_VERIFICATION=${SKIP_HTTP_VERIFICATION:-n} + - ONLY_MAILCOW_HOSTNAME=${ONLY_MAILCOW_HOSTNAME:-n} + - LE_STAGING=${LE_STAGING:-n} + - TZ=${TZ} + - REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-} + - REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-} + - REDISPASS=${REDISPASS} + - SNAT_TO_SOURCE=${SNAT_TO_SOURCE:-n} + - SNAT6_TO_SOURCE=${SNAT6_TO_SOURCE:-n} + volumes: + - ./data/web/.well-known/acme-challenge:/var/www/acme:z + - ./data/assets/ssl:/var/lib/acme/:z + - ./data/assets/ssl-example:/var/lib/ssl-example/:ro,Z + - mysql-socket-vol-1:/var/run/mysqld/ + restart: always + networks: + mailcow-network: + aliases: + - acme + + netfilter-mailcow: + image: ghcr.io/mailcow/netfilter:1.62 + stop_grace_period: 30s + restart: always + privileged: true + environment: + - TZ=${TZ} + - IPV4_NETWORK=${IPV4_NETWORK:-172.22.1} + - IPV6_NETWORK=${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64} + - SNAT_TO_SOURCE=${SNAT_TO_SOURCE:-n} + - SNAT6_TO_SOURCE=${SNAT6_TO_SOURCE:-n} + - REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-} + - REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-} + - REDISPASS=${REDISPASS} + - MAILCOW_REPLICA_IP=${MAILCOW_REPLICA_IP:-} + - DISABLE_NETFILTER_ISOLATION_RULE=${DISABLE_NETFILTER_ISOLATION_RULE:-n} + network_mode: "host" + volumes: + - /lib/modules:/lib/modules:ro + + watchdog-mailcow: + image: ghcr.io/mailcow/watchdog:2.07 + dns: + - ${IPV4_NETWORK:-172.22.1}.254 + tmpfs: + - /tmp + volumes: + - rspamd-vol-1:/var/lib/rspamd + - mysql-socket-vol-1:/var/run/mysqld/ + - postfix-vol-1:/var/spool/postfix + - ./data/assets/ssl:/etc/ssl/mail/:ro,z + restart: always + depends_on: + - postfix-mailcow + - dovecot-mailcow + - mysql-mailcow + - acme-mailcow + - redis-mailcow + environment: + - IPV6_NETWORK=${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64} + - LOG_LINES=${LOG_LINES:-9999} + - TZ=${TZ} + - DBNAME=${DBNAME} + - DBUSER=${DBUSER} + - DBPASS=${DBPASS} + - DBROOT=${DBROOT} + - USE_WATCHDOG=${USE_WATCHDOG:-n} + - WATCHDOG_NOTIFY_EMAIL=${WATCHDOG_NOTIFY_EMAIL:-} + - WATCHDOG_NOTIFY_BAN=${WATCHDOG_NOTIFY_BAN:-y} + - WATCHDOG_NOTIFY_START=${WATCHDOG_NOTIFY_START:-y} + - WATCHDOG_SUBJECT=${WATCHDOG_SUBJECT:-Watchdog ALERT} + - WATCHDOG_NOTIFY_WEBHOOK=${WATCHDOG_NOTIFY_WEBHOOK:-} + - WATCHDOG_NOTIFY_WEBHOOK_BODY=${WATCHDOG_NOTIFY_WEBHOOK_BODY:-} + - WATCHDOG_EXTERNAL_CHECKS=${WATCHDOG_EXTERNAL_CHECKS:-n} + - WATCHDOG_MYSQL_REPLICATION_CHECKS=${WATCHDOG_MYSQL_REPLICATION_CHECKS:-n} + - WATCHDOG_VERBOSE=${WATCHDOG_VERBOSE:-n} + - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} + - COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME:-mailcow-dockerized} + - IPV4_NETWORK=${IPV4_NETWORK:-172.22.1} + - IP_BY_DOCKER_API=${IP_BY_DOCKER_API:-0} + - CHECK_UNBOUND=${CHECK_UNBOUND:-1} + - SKIP_CLAMD=${SKIP_CLAMD:-n} + - SKIP_LETS_ENCRYPT=${SKIP_LETS_ENCRYPT:-n} + - SKIP_SOGO=${SKIP_SOGO:-n} + - HTTPS_PORT=${HTTPS_PORT:-443} + - REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-} + - REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-} + - REDISPASS=${REDISPASS} + - EXTERNAL_CHECKS_THRESHOLD=${EXTERNAL_CHECKS_THRESHOLD:-1} + - NGINX_THRESHOLD=${NGINX_THRESHOLD:-5} + - UNBOUND_THRESHOLD=${UNBOUND_THRESHOLD:-5} + - REDIS_THRESHOLD=${REDIS_THRESHOLD:-5} + - MYSQL_THRESHOLD=${MYSQL_THRESHOLD:-5} + - MYSQL_REPLICATION_THRESHOLD=${MYSQL_REPLICATION_THRESHOLD:-1} + - SOGO_THRESHOLD=${SOGO_THRESHOLD:-3} + - POSTFIX_THRESHOLD=${POSTFIX_THRESHOLD:-8} + - CLAMD_THRESHOLD=${CLAMD_THRESHOLD:-15} + - DOVECOT_THRESHOLD=${DOVECOT_THRESHOLD:-12} + - DOVECOT_REPL_THRESHOLD=${DOVECOT_REPL_THRESHOLD:-20} + - PHPFPM_THRESHOLD=${PHPFPM_THRESHOLD:-5} + - RATELIMIT_THRESHOLD=${RATELIMIT_THRESHOLD:-1} + - FAIL2BAN_THRESHOLD=${FAIL2BAN_THRESHOLD:-1} + - ACME_THRESHOLD=${ACME_THRESHOLD:-1} + - RSPAMD_THRESHOLD=${RSPAMD_THRESHOLD:-5} + - OLEFY_THRESHOLD=${OLEFY_THRESHOLD:-5} + - MAILQ_THRESHOLD=${MAILQ_THRESHOLD:-20} + - MAILQ_CRIT=${MAILQ_CRIT:-30} + networks: + mailcow-network: + aliases: + - watchdog + + dockerapi-mailcow: + image: ghcr.io/mailcow/dockerapi:2.11 + security_opt: + - label=disable + restart: always + dns: + - ${IPV4_NETWORK:-172.22.1}.254 + environment: + - DBROOT=${DBROOT} + - TZ=${TZ} + - REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-} + - REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-} + - REDISPASS=${REDISPASS} + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + networks: + mailcow-network: + aliases: + - dockerapi + + olefy-mailcow: + image: ghcr.io/mailcow/olefy:1.14 + restart: always + environment: + - TZ=${TZ} + - OLEFY_BINDADDRESS=0.0.0.0 + - OLEFY_BINDPORT=10055 + - OLEFY_TMPDIR=/tmp + - OLEFY_PYTHON_PATH=/usr/bin/python3 + - OLEFY_OLEVBA_PATH=/usr/bin/olevba + - OLEFY_LOGLVL=20 + - OLEFY_MINLENGTH=500 + - OLEFY_DEL_TMP=1 + networks: + mailcow-network: + aliases: + - olefy + + ofelia-mailcow: + image: mcuadros/ofelia:latest + restart: always + command: daemon --docker -f label=com.docker.compose.project=${COMPOSE_PROJECT_NAME} + environment: + - TZ=${TZ} + - COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME} + depends_on: + - sogo-mailcow + - dovecot-mailcow + labels: + ofelia.enabled: "true" + security_opt: + - label=disable + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + networks: + mailcow-network: + aliases: + - ofelia + + ipv6nat-mailcow: + depends_on: + - unbound-mailcow + - mysql-mailcow + - redis-mailcow + - clamd-mailcow + - rspamd-mailcow + - php-fpm-mailcow + - sogo-mailcow + - dovecot-mailcow + - postfix-mailcow + - memcached-mailcow + - nginx-mailcow + - acme-mailcow + - netfilter-mailcow + - watchdog-mailcow + - dockerapi-mailcow + environment: + - TZ=${TZ} + image: robbertkl/ipv6nat + security_opt: + - label=disable + restart: always + privileged: true + network_mode: "host" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - /lib/modules:/lib/modules:ro + +networks: + mailcow-network: + driver: bridge + driver_opts: + com.docker.network.bridge.name: br-mailcow + enable_ipv6: true + ipam: + driver: default + config: + - subnet: ${IPV4_NETWORK:-172.22.1}.0/24 + - subnet: ${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64} + +volumes: + vmail-vol-1: + vmail-index-vol-1: + mysql-vol-1: + mysql-socket-vol-1: + redis-vol-1: + rspamd-vol-1: + postfix-vol-1: + crypt-vol-1: + sogo-web-vol-1: + sogo-userdata-backup-vol-1: + clamd-db-vol-1: diff --git a/mailcow/generate_config.sh b/mailcow/generate_config.sh new file mode 100755 index 0000000..c7a7cb6 --- /dev/null +++ b/mailcow/generate_config.sh @@ -0,0 +1,596 @@ +#!/usr/bin/env bash + +set -o pipefail + +if [[ "$(uname -r)" =~ ^4\.15\.0-60 ]]; then + echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!"; + echo "Please update to 5.x or use another distribution." + exit 1 +fi + +if [[ "$(uname -r)" =~ ^4\.4\. ]]; then + if grep -q Ubuntu <<< "$(uname -a)"; then + echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!"; + echo "Please update to linux-generic-hwe-16.04 by running \"apt-get install --install-recommends linux-generic-hwe-16.04\"" + exit 1 + fi +fi + +if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo "BusyBox grep detected, please install gnu grep, \"apk add --no-cache --upgrade grep\""; exit 1; fi +# This will also cover sort +if cp --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo "BusyBox cp detected, please install coreutils, \"apk add --no-cache --upgrade coreutils\""; exit 1; fi +if sed --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo "BusyBox sed detected, please install gnu sed, \"apk add --no-cache --upgrade sed\""; exit 1; fi + +for bin in openssl curl docker git awk sha1sum grep cut; do + if [[ -z $(which ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi +done + +# Check Docker Version (need at least 24.X) +docker_version=$(docker version --format '{{.Server.Version}}' | cut -d '.' -f 1) + +if [[ $docker_version -lt 24 ]]; then + echo -e "\e[31mCannot find Docker with a Version higher or equals 24.0.0\e[0m" + echo -e "\e[33mmailcow needs a newer Docker version to work properly...\e[0m" + echo -e "\e[31mPlease update your Docker installation... exiting\e[0m" + exit 1 +fi + +if docker compose > /dev/null 2>&1; then + if docker compose version --short | grep -e "^2." -e "^v2." > /dev/null 2>&1; then + COMPOSE_VERSION=native + echo -e "\e[33mFound Docker Compose Plugin (native).\e[0m" + echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m" + sleep 2 + echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m" + else + echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" + echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m" + exit 1 + fi +elif docker-compose > /dev/null 2>&1; then + if ! [[ $(alias docker-compose 2> /dev/null) ]] ; then + if docker-compose version --short | grep "^2." > /dev/null 2>&1; then + COMPOSE_VERSION=standalone + echo -e "\e[33mFound Docker Compose Standalone.\e[0m" + echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to standalone\e[0m" + sleep 2 + echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.\e[0m" + else + echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" + echo -e "\e[31mPlease update/install manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m" + exit 1 + fi + fi + +else + echo -e "\e[31mCannot find Docker Compose.\e[0m" + echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m" + exit 1 +fi + +detect_bad_asn() { + echo -e "\e[33mDetecting if your IP is listed on Spamhaus Bad ASN List...\e[0m" + response=$(curl --connect-timeout 15 --max-time 30 -s -o /dev/null -w "%{http_code}" "https://asn-check.mailcow.email") + if [ "$response" -eq 503 ]; then + if [ -z "$SPAMHAUS_DQS_KEY" ]; then + echo -e "\e[33mYour server's public IP uses an AS that is blocked by Spamhaus to use their DNS public blocklists for Postfix.\e[0m" + echo -e "\e[33mmailcow did not detected a value for the variable SPAMHAUS_DQS_KEY inside mailcow.conf!\e[0m" + sleep 2 + echo "" + echo -e "\e[33mTo use the Spamhaus DNS Blocklists again, you will need to create a FREE account for their Data Query Service (DQS) at: https://www.spamhaus.com/free-trial/sign-up-for-a-free-data-query-service-account\e[0m" + echo -e "\e[33mOnce done, enter your DQS API key in mailcow.conf and mailcow will do the rest for you!\e[0m" + echo "" + sleep 2 + + else + echo -e "\e[33mYour server's public IP uses an AS that is blocked by Spamhaus to use their DNS public blocklists for Postfix.\e[0m" + echo -e "\e[32mmailcow detected a Value for the variable SPAMHAUS_DQS_KEY inside mailcow.conf. Postfix will use DQS with the given API key...\e[0m" + fi + elif [ "$response" -eq 200 ]; then + echo -e "\e[33mCheck completed! Your IP is \e[32mclean\e[0m" + elif [ "$response" -eq 429 ]; then + echo -e "\e[33mCheck completed! \e[31mYour IP seems to be rate limited on the ASN Check service... please try again later!\e[0m" + else + echo -e "\e[31mCheck failed! \e[0mMaybe a DNS or Network problem?\e[0m" + fi +} + +### If generate_config.sh is started with --dev or -d it will not check out nightly or master branch and will keep on the current branch +if [[ ${1} == "--dev" || ${1} == "-d" ]]; then + SKIP_BRANCH=y +else + SKIP_BRANCH=n +fi + +if [ -f mailcow.conf ]; then + read -r -p "A config file exists and will be overwritten, are you sure you want to continue? [y/N] " response + case $response in + [yY][eE][sS]|[yY]) + mv mailcow.conf mailcow.conf_backup + chmod 600 mailcow.conf_backup + ;; + *) + exit 1 + ;; + esac +fi + +echo "Press enter to confirm the detected value '[value]' where applicable or enter a custom value." +while [ -z "${MAILCOW_HOSTNAME}" ]; do + read -p "Mail server hostname (FQDN) - this is not your mail domain, but your mail servers hostname: " -e MAILCOW_HOSTNAME + DOTS=${MAILCOW_HOSTNAME//[^.]}; + if [ ${#DOTS} -lt 1 ]; then + echo -e "\e[31mMAILCOW_HOSTNAME (${MAILCOW_HOSTNAME}) is not a FQDN!\e[0m" + sleep 1 + echo "Please change it to a FQDN and redeploy the stack with docker(-)compose up -d" + exit 1 + elif [[ "${MAILCOW_HOSTNAME: -1}" == "." ]]; then + echo "MAILCOW_HOSTNAME (${MAILCOW_HOSTNAME}) is ending with a dot. This is not a valid FQDN!" + exit 1 + elif [ ${#DOTS} -eq 1 ]; then + echo -e "\e[33mMAILCOW_HOSTNAME (${MAILCOW_HOSTNAME}) does not contain a Subdomain. This is not fully tested and may cause issues.\e[0m" + echo "Find more information about why this message exists here: https://github.com/mailcow/mailcow-dockerized/issues/1572" + read -r -p "Do you want to proceed anyway? [y/N] " response + if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + echo "OK. Procceding." + else + echo "OK. Exiting." + exit 1 + fi + fi +done + +if [ -a /etc/timezone ]; then + DETECTED_TZ=$(cat /etc/timezone) +elif [ -a /etc/localtime ]; then + DETECTED_TZ=$(readlink /etc/localtime|sed -n 's|^.*zoneinfo/||p') +fi + +while [ -z "${MAILCOW_TZ}" ]; do + if [ -z "${DETECTED_TZ}" ]; then + read -p "Timezone: " -e MAILCOW_TZ + else + read -p "Timezone [${DETECTED_TZ}]: " -e MAILCOW_TZ + [ -z "${MAILCOW_TZ}" ] && MAILCOW_TZ=${DETECTED_TZ} + fi +done + +MEM_TOTAL=$(awk '/MemTotal/ {print $2}' /proc/meminfo) + +if [ -z "${SKIP_CLAMD}" ]; then + if [ "${MEM_TOTAL}" -le "2621440" ]; then + echo "Installed memory is <= 2.5 GiB. It is recommended to disable ClamAV to prevent out-of-memory situations." + echo "ClamAV can be re-enabled by setting SKIP_CLAMD=n in mailcow.conf." + read -r -p "Do you want to disable ClamAV now? [Y/n] " response + case $response in + [nN][oO]|[nN]) + SKIP_CLAMD=n + ;; + *) + SKIP_CLAMD=y + ;; + esac + else + SKIP_CLAMD=n + fi +fi + +if [[ ${SKIP_BRANCH} != y ]]; then + echo "Which branch of mailcow do you want to use?" + echo "" + echo "Available Branches:" + echo "- master branch (stable updates) | default, recommended [1]" + echo "- nightly branch (unstable updates, testing) | not-production ready [2]" + echo "- legacy branch (supported until February 2026) | deprecated, security updates only [3]" + sleep 1 + + while [ -z "${MAILCOW_BRANCH}" ]; do + read -r -p "Choose the Branch with it's number [1/2/3] " branch + case $branch in + [3]) + MAILCOW_BRANCH="legacy" + ;; + [2]) + MAILCOW_BRANCH="nightly" + ;; + *) + MAILCOW_BRANCH="master" + ;; + esac + done + + git fetch --all + git checkout -f "$MAILCOW_BRANCH" + +elif [[ ${SKIP_BRANCH} == y ]]; then + echo -e "\033[33mEnabled Dev Mode.\033[0m" + echo -e "\033[33mNot checking out a different branch!\033[0m" + MAILCOW_BRANCH=$(git rev-parse --short $(git rev-parse @{upstream})) + +else + echo -e "\033[31mCould not determine branch input..." + echo -e "\033[31mExiting." + exit 1 +fi + +if [ ! -z "${MAILCOW_BRANCH}" ]; then + git_branch=${MAILCOW_BRANCH} +fi + +[ ! -f ./data/conf/rspamd/override.d/worker-controller-password.inc ] && echo '# Placeholder' > ./data/conf/rspamd/override.d/worker-controller-password.inc + +cat << EOF > mailcow.conf +# ------------------------------ +# mailcow web ui configuration +# ------------------------------ +# example.org is _not_ a valid hostname, use a fqdn here. +# Default admin user is "admin" +# Default password is "moohoo" + +MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} + +# Password hash algorithm +# Only certain password hash algorithm are supported. For a fully list of supported schemes, +# see https://docs.mailcow.email/models/model-passwd/ +MAILCOW_PASS_SCHEME=BLF-CRYPT + +# ------------------------------ +# SQL database configuration +# ------------------------------ + +DBNAME=mailcow +DBUSER=mailcow + +# Please use long, random alphanumeric strings (A-Za-z0-9) + +DBPASS=$(LC_ALL=C /dev/null | head -c 28) +DBROOT=$(LC_ALL=C /dev/null | head -c 28) + +# ------------------------------ +# REDIS configuration +# ------------------------------ + +REDISPASS=$(LC_ALL=C /dev/null | head -c 28) + +# ------------------------------ +# HTTP/S Bindings +# ------------------------------ + +# You should use HTTPS, but in case of SSL offloaded reverse proxies: +# Might be important: This will also change the binding within the container. +# If you use a proxy within Docker, point it to the ports you set below. +# Do _not_ use IP:PORT in HTTP(S)_BIND or HTTP(S)_PORT +# IMPORTANT: Do not use port 8081, 9081, 9082 or 65510! +# Example: HTTP_BIND=1.2.3.4 +# For IPv4 leave it as it is: HTTP_BIND= & HTTPS_PORT= +# For IPv6 see https://docs.mailcow.email/post_installation/firststeps-ip_bindings/ + +HTTP_PORT=80 +HTTP_BIND= + +HTTPS_PORT=443 +HTTPS_BIND= + +# Redirect HTTP connections to HTTPS - y/n +HTTP_REDIRECT=n + +# ------------------------------ +# Other bindings +# ------------------------------ +# You should leave that alone +# Format: 11.22.33.44:25 or 12.34.56.78:465 etc. + +SMTP_PORT=25 +SMTPS_PORT=465 +SUBMISSION_PORT=587 +IMAP_PORT=143 +IMAPS_PORT=993 +POP_PORT=110 +POPS_PORT=995 +SIEVE_PORT=4190 +DOVEADM_PORT=127.0.0.1:19991 +SQL_PORT=127.0.0.1:13306 +REDIS_PORT=127.0.0.1:7654 + +# Your timezone +# See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones for a list of timezones +# Use the column named 'TZ identifier' + pay attention for the column named 'Notes' + +TZ=${MAILCOW_TZ} + +# Fixed project name +# Please use lowercase letters only + +COMPOSE_PROJECT_NAME=mailcowdockerized + +# Used Docker Compose version +# Switch here between native (compose plugin) and standalone +# For more informations take a look at the mailcow docs regarding the configuration options. +# Normally this should be untouched but if you decided to use either of those you can switch it manually here. +# Please be aware that at least one of those variants should be installed on your machine or mailcow will fail. + +DOCKER_COMPOSE_VERSION=${COMPOSE_VERSION} + +# Set this to "allow" to enable the anyone pseudo user. Disabled by default. +# When enabled, ACL can be created, that apply to "All authenticated users" +# This should probably only be activated on mail hosts, that are used exclusivly by one organisation. +# Otherwise a user might share data with too many other users. +ACL_ANYONE=disallow + +# Garbage collector cleanup +# Deleted domains and mailboxes are moved to /var/vmail/_garbage/timestamp_sanitizedstring +# How long should objects remain in the garbage until they are being deleted? (value in minutes) +# Check interval is hourly + +MAILDIR_GC_TIME=7200 + +# Additional SAN for the certificate +# +# You can use wildcard records to create specific names for every domain you add to mailcow. +# Example: Add domains "example.com" and "example.net" to mailcow, change ADDITIONAL_SAN to a value like: +#ADDITIONAL_SAN=imap.*,smtp.* +# This will expand the certificate to "imap.example.com", "smtp.example.com", "imap.example.net", "smtp.example.net" +# plus every domain you add in the future. +# +# You can also just add static names... +#ADDITIONAL_SAN=srv1.example.net +# ...or combine wildcard and static names: +#ADDITIONAL_SAN=imap.*,srv1.example.com +# + +ADDITIONAL_SAN= + +# Obtain certificates for autodiscover.* and autoconfig.* domains. +# This can be useful to switch off in case you are in a scenario where a reverse proxy already handles those. +# There are mixed scenarios where ports 80,443 are occupied and you do not want to share certs +# between services. So acme-mailcow obtains for maildomains and all web-things get handled +# in the reverse proxy. +AUTODISCOVER_SAN=y + +# Additional server names for mailcow UI +# +# Specify alternative addresses for the mailcow UI to respond to +# This is useful when you set mail.* as ADDITIONAL_SAN and want to make sure mail.maildomain.com will always point to the mailcow UI. +# If the server name does not match a known site, Nginx decides by best-guess and may redirect users to the wrong web root. +# You can understand this as server_name directive in Nginx. +# Comma separated list without spaces! Example: ADDITIONAL_SERVER_NAMES=a.b.c,d.e.f + +ADDITIONAL_SERVER_NAMES= + +# Skip running ACME (acme-mailcow, Let's Encrypt certs) - y/n + +SKIP_LETS_ENCRYPT=n + +# Create seperate certificates for all domains - y/n +# this will allow adding more than 100 domains, but some email clients will not be able to connect with alternative hostnames +# see https://doc.dovecot.org/admin_manual/ssl/sni_support +ENABLE_SSL_SNI=n + +# Skip IPv4 check in ACME container - y/n + +SKIP_IP_CHECK=n + +# Skip HTTP verification in ACME container - y/n + +SKIP_HTTP_VERIFICATION=n + +# Skip Unbound (DNS Resolver) Healthchecks (NOT Recommended!) - y/n + +SKIP_UNBOUND_HEALTHCHECK=n + +# Skip ClamAV (clamd-mailcow) anti-virus (Rspamd will auto-detect a missing ClamAV container) - y/n + +SKIP_CLAMD=${SKIP_CLAMD} + +# Skip SOGo: Will disable SOGo integration and therefore webmail, DAV protocols and ActiveSync support (experimental, unsupported, not fully implemented) - y/n + +SKIP_SOGO=n + +# Skip FTS (Fulltext Search) for Dovecot on low-memory, low-threaded systems or if you simply want to disable it. +# Dovecot inside mailcow use Flatcurve as FTS Backend. + +SKIP_FTS=n + +# Dovecot Indexing (FTS) Process maximum heap size in MB, there is no recommendation, please see Dovecot docs. +# Flatcurve (Xapian backend) is used as the FTS Indexer. It is supposed to be efficient in CPU and RAM consumption. +# However: Please always monitor your Resource consumption! + +FTS_HEAP=128 + +# Controls how many processes the Dovecot indexing process can spawn at max. +# Too many indexing processes can use a lot of CPU and Disk I/O. +# Please visit: https://doc.dovecot.org/configuration_manual/service_configuration/#indexer-worker for more informations + +FTS_PROCS=1 + +# Allow admins to log into SOGo as email user (without any password) + +ALLOW_ADMIN_EMAIL_LOGIN=n + +# Enable watchdog (watchdog-mailcow) to restart unhealthy containers + +USE_WATCHDOG=y + +# Send watchdog notifications by mail (sent from watchdog@MAILCOW_HOSTNAME) +# CAUTION: +# 1. You should use external recipients +# 2. Mails are sent unsigned (no DKIM) +# 3. If you use DMARC, create a separate DMARC policy ("v=DMARC1; p=none;" in _dmarc.MAILCOW_HOSTNAME) +# Multiple rcpts allowed, NO quotation marks, NO spaces + +#WATCHDOG_NOTIFY_EMAIL=a@example.com,b@example.com,c@example.com +#WATCHDOG_NOTIFY_EMAIL= + +# Send notifications to a webhook URL that receives a POST request with the content type "application/json". +# You can use this to send notifications to services like Discord, Slack and others. +#WATCHDOG_NOTIFY_WEBHOOK=https://discord.com/api/webhooks/XXXXXXXXXXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +# JSON body included in the webhook POST request. Needs to be in single quotes. +# Following variables are available: SUBJECT, BODY +#WATCHDOG_NOTIFY_WEBHOOK_BODY='{"username": "mailcow Watchdog", "content": "**${SUBJECT}**\n${BODY}"}' + +# Notify about banned IP (includes whois lookup) +WATCHDOG_NOTIFY_BAN=n + +# Send a notification when the watchdog is started. +WATCHDOG_NOTIFY_START=y + +# Subject for watchdog mails. Defaults to "Watchdog ALERT" followed by the error message. +#WATCHDOG_SUBJECT= + +# Checks if mailcow is an open relay. Requires a SAL. More checks will follow. +# https://www.servercow.de/mailcow?lang=en +# https://www.servercow.de/mailcow?lang=de +# No data is collected. Opt-in and anonymous. +# Will only work with unmodified mailcow setups. +WATCHDOG_EXTERNAL_CHECKS=n + +# Enable watchdog verbose logging +WATCHDOG_VERBOSE=n + +# Max log lines per service to keep in Redis logs + +LOG_LINES=9999 + +# Internal IPv4 /24 subnet, format n.n.n (expands to n.n.n.0/24) +# Use private IPv4 addresses only, see https://en.wikipedia.org/wiki/Private_network#Private_IPv4_addresses + +IPV4_NETWORK=172.22.1 + +# Internal IPv6 subnet in fc00::/7 +# Use private IPv6 addresses only, see https://en.wikipedia.org/wiki/Private_network#Private_IPv6_addresses + +IPV6_NETWORK=fd4d:6169:6c63:6f77::/64 + +# Use this IPv4 for outgoing connections (SNAT) + +#SNAT_TO_SOURCE= + +# Use this IPv6 for outgoing connections (SNAT) + +#SNAT6_TO_SOURCE= + +# Create or override an API key for the web UI +# You _must_ define API_ALLOW_FROM, which is a comma separated list of IPs +# An API key defined as API_KEY has read-write access +# An API key defined as API_KEY_READ_ONLY has read-only access +# Allowed chars for API_KEY and API_KEY_READ_ONLY: a-z, A-Z, 0-9, - +# You can define API_KEY and/or API_KEY_READ_ONLY + +#API_KEY= +#API_KEY_READ_ONLY= +#API_ALLOW_FROM=172.22.1.1,127.0.0.1 + +# mail_home is ~/Maildir +MAILDIR_SUB=Maildir + +# SOGo session timeout in minutes +SOGO_EXPIRE_SESSION=480 + +# DOVECOT_MASTER_USER and DOVECOT_MASTER_PASS must both be provided. No special chars. +# Empty by default to auto-generate master user and password on start. +# User expands to DOVECOT_MASTER_USER@mailcow.local +# LEAVE EMPTY IF UNSURE +DOVECOT_MASTER_USER= +# LEAVE EMPTY IF UNSURE +DOVECOT_MASTER_PASS= + +# Let's Encrypt registration contact information +# Optional: Leave empty for none +# This value is only used on first order! +# Setting it at a later point will require the following steps: +# https://docs.mailcow.email/troubleshooting/debug-reset_tls/ +ACME_CONTACT= + +# WebAuthn device manufacturer verification +# After setting WEBAUTHN_ONLY_TRUSTED_VENDORS=y only devices from trusted manufacturers are allowed +# root certificates can be placed for validation under mailcow-dockerized/data/web/inc/lib/WebAuthn/rootCertificates +WEBAUTHN_ONLY_TRUSTED_VENDORS=n + +# Spamhaus Data Query Service Key +# Optional: Leave empty for none +# Enter your key here if you are using a blocked ASN (OVH, AWS, Cloudflare e.g) for the unregistered Spamhaus Blocklist. +# If empty, it will completely disable Spamhaus blocklists if it detects that you are running on a server using a blocked AS. +# Otherwise it will work normally. +SPAMHAUS_DQS_KEY= + +# Prevent netfilter from setting an iptables/nftables rule to isolate the mailcow docker network - y/n +# CAUTION: Disabling this may expose container ports to other neighbors on the same subnet, even if the ports are bound to localhost +DISABLE_NETFILTER_ISOLATION_RULE=n +EOF + +mkdir -p data/assets/ssl + +chmod 600 mailcow.conf + +# copy but don't overwrite existing certificate +echo "Generating snake-oil certificate..." +# Making Willich more popular +openssl req -x509 -newkey rsa:4096 -keyout data/assets/ssl-example/key.pem -out data/assets/ssl-example/cert.pem -days 365 -subj "/C=DE/ST=NRW/L=Willich/O=mailcow/OU=mailcow/CN=${MAILCOW_HOSTNAME}" -sha256 -nodes +echo "Copying snake-oil certificate..." +cp -n -d data/assets/ssl-example/*.pem data/assets/ssl/ + +# Set app_info.inc.php +case ${git_branch} in + master) + mailcow_git_version=$(git describe --tags `git rev-list --tags --max-count=1`) + ;; + nightly) + mailcow_git_version=$(git rev-parse --short $(git rev-parse @{upstream})) + mailcow_last_git_version="" + ;; + legacy) + mailcow_git_version=$(git rev-parse --short $(git rev-parse @{upstream})) + mailcow_last_git_version="" + ;; + *) + mailcow_git_version=$(git rev-parse --short HEAD) + mailcow_last_git_version="" + ;; +esac +# if [ ${git_branch} == "master" ]; then +# mailcow_git_version=$(git describe --tags `git rev-list --tags --max-count=1`) +# elif [ ${git_branch} == "nightly" ]; then +# mailcow_git_version=$(git rev-parse --short $(git rev-parse @{upstream})) +# mailcow_last_git_version="" +# else +# mailcow_git_version=$(git rev-parse --short HEAD) +# mailcow_last_git_version="" +# fi + +if [[ $SKIP_BRANCH != "y" ]]; then +mailcow_git_commit=$(git rev-parse origin/${git_branch}) +mailcow_git_commit_date=$(git log -1 --format=%ci @{upstream} ) +else +mailcow_git_commit=$(git rev-parse ${git_branch}) +mailcow_git_commit_date=$(git log -1 --format=%ci @{upstream} ) +git_branch=$(git rev-parse --abbrev-ref HEAD) +fi + +if [ $? -eq 0 ]; then + echo ' data/web/inc/app_info.inc.php + echo ' $MAILCOW_GIT_VERSION="'$mailcow_git_version'";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_LAST_GIT_VERSION="";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_GIT_OWNER="mailcow";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_GIT_REPO="mailcow-dockerized";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_GIT_URL="https://github.com/mailcow/mailcow-dockerized";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_GIT_COMMIT="'$mailcow_git_commit'";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_GIT_COMMIT_DATE="'$mailcow_git_commit_date'";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_BRANCH="'$git_branch'";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_UPDATEDAT='$(date +%s)';' >> data/web/inc/app_info.inc.php + echo '?>' >> data/web/inc/app_info.inc.php +else + echo ' data/web/inc/app_info.inc.php + echo ' $MAILCOW_GIT_VERSION="'$mailcow_git_version'";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_LAST_GIT_VERSION="";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_GIT_OWNER="mailcow";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_GIT_REPO="mailcow-dockerized";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_GIT_URL="https://github.com/mailcow/mailcow-dockerized";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_GIT_COMMIT="";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_GIT_COMMIT_DATE="";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_BRANCH="'$git_branch'";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_UPDATEDAT='$(date +%s)';' >> data/web/inc/app_info.inc.php + echo '?>' >> data/web/inc/app_info.inc.php + echo -e "\e[33mCannot determine current git repository version...\e[0m" +fi + +detect_bad_asn diff --git a/mailcow/helper-scripts/_cold-standby.sh b/mailcow/helper-scripts/_cold-standby.sh new file mode 100755 index 0000000..bfda3ba --- /dev/null +++ b/mailcow/helper-scripts/_cold-standby.sh @@ -0,0 +1,300 @@ +#!/usr/bin/env bash + +PATH=${PATH}:/opt/bin +DATE=$(date +%Y-%m-%d_%H_%M_%S) +LOCAL_ARCH=$(uname -m) +export LC_ALL=C + +echo +echo "If this script is run automatically by cron or a timer AND you are using block-level snapshots on your backup destination, make sure both do not run at the same time." +echo "The snapshots of your backup destination should run AFTER the cold standby script finished to ensure consistent snapshots." +echo + +function preflight_local_checks() { + if [[ -z "${REMOTE_SSH_KEY}" ]]; then + >&2 echo -e "\e[31mREMOTE_SSH_KEY is not set\e[0m" + exit 1 + fi + + if [[ ! -s "${REMOTE_SSH_KEY}" ]]; then + >&2 echo -e "\e[31mKeyfile ${REMOTE_SSH_KEY} is empty\e[0m" + exit 1 + fi + + if [[ $(stat -c "%a" "${REMOTE_SSH_KEY}") -ne 600 ]]; then + >&2 echo -e "\e[31mKeyfile ${REMOTE_SSH_KEY} has insecure permissions\e[0m" + exit 1 + fi + + if [[ ! -z "${REMOTE_SSH_PORT}" ]]; then + if [[ ${REMOTE_SSH_PORT} != ?(-)+([0-9]) ]] || [[ ${REMOTE_SSH_PORT} -gt 65535 ]]; then + >&2 echo -e "\e[31mREMOTE_SSH_PORT is set but not an integer < 65535\e[0m" + exit 1 + fi + fi + + if [[ -z "${REMOTE_SSH_HOST}" ]]; then + >&2 echo -e "\e[31mREMOTE_SSH_HOST cannot be empty\e[0m" + exit 1 + fi + + for bin in rsync docker grep cut; do + if [[ -z $(which ${bin}) ]]; then + >&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m" + exit 1 + fi + done + + if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then + echo -e "\e[31mBusyBox grep detected on local system, please install GNU grep\e[0m" + exit 1 + fi +} + +function preflight_remote_checks() { + + if ! ssh -o StrictHostKeyChecking=no \ + -i "${REMOTE_SSH_KEY}" \ + ${REMOTE_SSH_HOST} \ + -p ${REMOTE_SSH_PORT} \ + rsync --version > /dev/null ; then + >&2 echo -e "\e[31mCould not verify connection to ${REMOTE_SSH_HOST}\e[0m" + >&2 echo -e "\e[31mPlease check the output above (is rsync >= 3.1.0 installed on the remote system?)\e[0m" + exit 1 + fi + + if ssh -o StrictHostKeyChecking=no \ + -i "${REMOTE_SSH_KEY}" \ + ${REMOTE_SSH_HOST} \ + -p ${REMOTE_SSH_PORT} \ + grep --help 2>&1 | head -n 1 | grep -q -i "busybox" ; then + >&2 echo -e "\e[31mBusyBox grep detected on remote system ${REMOTE_SSH_HOST}, please install GNU grep\e[0m" + exit 1 + fi + + for bin in rsync docker; do + if ! ssh -o StrictHostKeyChecking=no \ + -i "${REMOTE_SSH_KEY}" \ + ${REMOTE_SSH_HOST} \ + -p ${REMOTE_SSH_PORT} \ + which ${bin} > /dev/null ; then + >&2 echo -e "\e[31mCannot find ${bin} in remote PATH, exiting...\e[0m" + exit 1 + fi + done + + ssh -o StrictHostKeyChecking=no \ + -i "${REMOTE_SSH_KEY}" \ + ${REMOTE_SSH_HOST} \ + -p ${REMOTE_SSH_PORT} \ + "bash -s" << "EOF" +if docker compose > /dev/null 2>&1; then + exit 0 +elif docker-compose version --short | grep "^2." > /dev/null 2>&1; then + exit 1 +else +exit 2 +fi +EOF + +if [ $? = 0 ]; then + COMPOSE_COMMAND="docker compose" + echo "INFO: Using native docker compose on remote" + +elif [ $? = 1 ]; then + COMPOSE_COMMAND="docker-compose" + echo "INFO: Using standalone docker compose on remote" + +else + echo -e "\e[31mCannot find any Docker Compose on remote, exiting...\e[0m" + exit 1 +fi + + REMOTE_ARCH=$(ssh -o StrictHostKeyChecking=no -i "${REMOTE_SSH_KEY}" ${REMOTE_SSH_HOST} -p ${REMOTE_SSH_PORT} "uname -m") + +} + +SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +source "${SCRIPT_DIR}/../mailcow.conf" +COMPOSE_FILE="${SCRIPT_DIR}/../docker-compose.yml" +CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd 'A-Za-z-_') +SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' "${COMPOSE_FILE}") + +preflight_local_checks +preflight_remote_checks + +echo +echo -e "\033[1mFound compose project name ${CMPS_PRJ} for ${MAILCOW_HOSTNAME}\033[0m" +echo -e "\033[1mFound SQL ${SQLIMAGE}\033[0m" +echo + +# Print Message if Local Arch and Remote Arch is not the same +if [[ $LOCAL_ARCH != $REMOTE_ARCH ]]; then + echo + echo -e "\e[1;33m!!!!!!!!!!!!!!!!!!!!!!!!!! CAUTION !!!!!!!!!!!!!!!!!!!!!!!!!!\e[0m" + echo -e "\e[3;33mDetected Architecture missmatch from source to destination...\e[0m" + echo -e "\e[3;33mYour backup is transferred but some volumes might be skipped!\e[0m" + echo -e "\e[1;33m!!!!!!!!!!!!!!!!!!!!!!!!!! CAUTION !!!!!!!!!!!!!!!!!!!!!!!!!!\e[0m" + echo + sleep 2 +fi + +# Make sure destination exists, rsync can fail under some circumstances +echo -e "\033[1mPreparing remote...\033[0m" +if ! ssh -o StrictHostKeyChecking=no \ + -i "${REMOTE_SSH_KEY}" \ + ${REMOTE_SSH_HOST} \ + -p ${REMOTE_SSH_PORT} \ + mkdir -p "${SCRIPT_DIR}/../" ; then + >&2 echo -e "\e[31m[ERR]\e[0m - Could not prepare remote for mailcow base directory transfer" + exit 1 +fi + +# Syncing the mailcow base directory +echo -e "\033[1mSynchronizing mailcow base directory...\033[0m" +rsync --delete -aH -e "ssh -o StrictHostKeyChecking=no \ + -i \"${REMOTE_SSH_KEY}\" \ + -p ${REMOTE_SSH_PORT}" \ + "${SCRIPT_DIR}/../" root@${REMOTE_SSH_HOST}:"${SCRIPT_DIR}/../" +ec=$? +if [ ${ec} -ne 0 ] && [ ${ec} -ne 24 ]; then + >&2 echo -e "\e[31m[ERR]\e[0m - Could not transfer mailcow base directory to remote" + exit 1 +fi + +# Let the remote side create all network, volumes and containers to prevent need for external:true # +echo -e "\e[33mCreating networks, volumes and containers on remote...\e[0m" + +if ! ssh -o StrictHostKeyChecking=no \ + -i "${REMOTE_SSH_KEY}" \ + ${REMOTE_SSH_HOST} \ + -p ${REMOTE_SSH_PORT} \ + ${COMPOSE_COMMAND} -f "${SCRIPT_DIR}/../docker-compose.yml" create 2>&1 ; then + >&2 echo -e "\e[31m[ERR]\e[0m - Could not create networks, volumes and containers on remote" +fi + +# Trigger a Redis save for a consistent Redis copy +echo -ne "\033[1mRunning redis-cli save... \033[0m" +docker exec $(docker ps -qf name=redis-mailcow) redis-cli -a ${REDISPASS} --no-auth-warning save + +# Syncing volumes related to compose project +# Same here: make sure destination exists +for vol in $(docker volume ls -qf name="${CMPS_PRJ}"); do + + mountpoint="$(docker inspect ${vol} | grep Mountpoint | cut -d '"' -f4)" + + echo -e "\033[1mCreating remote mountpoint ${mountpoint} for ${vol}...\033[0m" + + ssh -o StrictHostKeyChecking=no \ + -i "${REMOTE_SSH_KEY}" \ + ${REMOTE_SSH_HOST} \ + -p ${REMOTE_SSH_PORT} \ + mkdir -p "${mountpoint}" + + if [[ "${vol}" =~ "mysql-vol-1" ]]; then + + # Make sure a previous backup does not exist + rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/" + + echo -e "\033[1mCreating consistent backup of MariaDB volume...\033[0m" + if ! docker run --rm \ + --network $(docker network ls -qf name=${CMPS_PRJ}_) \ + -v $(docker volume ls -qf name=${CMPS_PRJ}_mysql-vol-1):/var/lib/mysql/:ro \ + --entrypoint= \ + -v "${SCRIPT_DIR}/../_tmp_mariabackup":/backup \ + ${SQLIMAGE} mariabackup --host mysql --user root --password ${DBROOT} --backup --target-dir=/backup 2>/dev/null ; then + >&2 echo -e "\e[31m[ERR]\e[0m - Could not create MariaDB backup on source" + rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/" + exit 1 + fi + + if ! docker run --rm \ + --network $(docker network ls -qf name=${CMPS_PRJ}_) \ + --entrypoint= \ + -v "${SCRIPT_DIR}/../_tmp_mariabackup":/backup \ + ${SQLIMAGE} mariabackup --prepare --target-dir=/backup 2> /dev/null ; then + >&2 echo -e "\e[31m[ERR]\e[0m - Could not transfer MariaDB backup to remote" + rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/" + exit 1 + fi + + chown -R 999:999 "${SCRIPT_DIR}/../_tmp_mariabackup" + + echo -e "\033[1mSynchronizing MariaDB backup...\033[0m" + rsync --delete --info=progress2 -aH -e "ssh -o StrictHostKeyChecking=no \ + -i \"${REMOTE_SSH_KEY}\" \ + -p ${REMOTE_SSH_PORT}" \ + "${SCRIPT_DIR}/../_tmp_mariabackup/" root@${REMOTE_SSH_HOST}:"${mountpoint}" + ec=$? + if [ ${ec} -ne 0 ] && [ ${ec} -ne 24 ]; then + >&2 echo -e "\e[31m[ERR]\e[0m - Could not transfer MariaDB backup to remote" + exit 1 + fi + + # Cleanup + rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/" + + elif [[ "${vol}" =~ "rspamd-vol-1" ]]; then + # Exclude rspamd-vol-1 if the Architectures are not the same on source and destination due to compatibility issues. + if [[ $LOCAL_ARCH == $REMOTE_ARCH ]]; then + echo -e "\033[1mSynchronizing ${vol} from local ${mountpoint}...\033[0m" + rsync --delete --info=progress2 -aH -e "ssh -o StrictHostKeyChecking=no \ + -i \"${REMOTE_SSH_KEY}\" \ + -p ${REMOTE_SSH_PORT}" \ + "${mountpoint}/" root@${REMOTE_SSH_HOST}:"${mountpoint}" + else + echo -e "\e[1;31mSkipping ${vol} from local maschine due to incompatiblity between different architecture...\e[0m" + sleep 2 + continue + fi + + else + echo -e "\033[1mSynchronizing ${vol} from local ${mountpoint}...\033[0m" + rsync --delete --info=progress2 -aH -e "ssh -o StrictHostKeyChecking=no \ + -i \"${REMOTE_SSH_KEY}\" \ + -p ${REMOTE_SSH_PORT}" \ + "${mountpoint}/" root@${REMOTE_SSH_HOST}:"${mountpoint}" + ec=$? + if [ ${ec} -ne 0 ] && [ ${ec} -ne 24 ]; then + >&2 echo -e "\e[31m[ERR]\e[0m - Could not transfer ${vol} from local ${mountpoint} to remote" + exit 1 + fi + fi + + echo -e "\e[32mCompleted\e[0m" + +done + +# Restart Dockerd on destination +echo -ne "\033[1mRestarting Docker daemon on remote to detect new volumes... \033[0m" +if ! ssh -o StrictHostKeyChecking=no \ + -i "${REMOTE_SSH_KEY}" \ + ${REMOTE_SSH_HOST} \ + -p ${REMOTE_SSH_PORT} \ + systemctl restart docker ; then + >&2 echo -e "\e[31m[ERR]\e[0m - Could not restart Docker daemon on remote" + exit 1 +fi +echo "OK" + + echo -e "\e[33mPulling images on remote...\e[0m" + echo -e "\e[33mProcess is NOT stuck! Please wait...\e[0m" + + if ! ssh -o StrictHostKeyChecking=no \ + -i "${REMOTE_SSH_KEY}" \ + ${REMOTE_SSH_HOST} \ + -p ${REMOTE_SSH_PORT} \ + ${COMPOSE_COMMAND} -f "${SCRIPT_DIR}/../docker-compose.yml" pull --quiet 2>&1 ; then + >&2 echo -e "\e[31m[ERR]\e[0m - Could not pull images on remote" + fi + +echo -e "\033[1mExecuting update script and forcing garbage cleanup on remote...\033[0m" +if ! ssh -o StrictHostKeyChecking=no \ + -i "${REMOTE_SSH_KEY}" \ + ${REMOTE_SSH_HOST} \ + -p ${REMOTE_SSH_PORT} \ + ${SCRIPT_DIR}/../update.sh -f --gc ; then + >&2 echo -e "\e[31m[ERR]\e[0m - Could not cleanup old images on remote" +fi + +echo -e "\e[32mDone\e[0m" diff --git a/mailcow/helper-scripts/add-new-lang-keys.php b/mailcow/helper-scripts/add-new-lang-keys.php new file mode 100644 index 0000000..1f5f377 --- /dev/null +++ b/mailcow/helper-scripts/add-new-lang-keys.php @@ -0,0 +1,63 @@ + $v) { + if (is_array($arr1[$k]) && is_array($arr2[$k])) { + $d = array_diff_key_recursive($arr1[$k], $arr2[$k]); + + if ($d) { + $diff[$k] = $d; + } + } + } + + return $diff; +} + +// target lang +$targetLang = $argv[1]; + +if(empty($targetLang)) { + die('Please specify target lang as the first argument, to which you want to add missing keys from master lang (EN). Use the lowercase name, + for example `sk` for the Slovak language'."\n"); +} + +// load master lang +$masterLang = file_get_contents(__DIR__.'/../data/web/lang/lang.en-gb.json'); +$masterLang = json_decode($masterLang, true); + +// load target lang +$lang = file_get_contents(__DIR__.'/../data/web/lang/lang.'.$targetLang.'.json'); +$lang = json_decode($lang, true); + +// compare lang keys +$result = array_diff_key_recursive($masterLang, $lang); + +if(empty($result)) { + die('No new keys were added. Looks like target lang is up to date.'."\n"); +} + +foreach($result as $key => $val) { + // check if section key exists in target lang + if(array_key_exists($key, $lang)) { + // add only missing section keys + foreach ($val as $k => $v) { + $lang[$key][$k] = $v; + } + // sort keys + ksort($lang[$key]); + } else { + // add whole section + $lang[$key] = $val; + ksort($lang); + } +} + +$lang = json_encode($lang, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); +file_put_contents(__DIR__.'/../data/web/lang/lang.'.$targetLang.'.json', $lang); + +echo 'Following new lang keys were added and need translation:'."\n"; +print_r($result); diff --git a/mailcow/helper-scripts/backup_and_restore.sh b/mailcow/helper-scripts/backup_and_restore.sh new file mode 100755 index 0000000..0cb37fc --- /dev/null +++ b/mailcow/helper-scripts/backup_and_restore.sh @@ -0,0 +1,410 @@ +#!/usr/bin/env bash + +DEBIAN_DOCKER_IMAGE="ghcr.io/mailcow/backup:latest" + +if [[ ! -z ${MAILCOW_BACKUP_LOCATION} ]]; then + BACKUP_LOCATION="${MAILCOW_BACKUP_LOCATION}" +fi + +if [[ ! ${1} =~ (backup|restore) ]]; then + echo "First parameter needs to be 'backup' or 'restore'" + exit 1 +fi + +if [[ ${1} == "backup" && ! ${2} =~ (crypt|vmail|redis|rspamd|postfix|mysql|all|--delete-days) ]]; then + echo "Second parameter needs to be 'vmail', 'crypt', 'redis', 'rspamd', 'postfix', 'mysql', 'all' or '--delete-days'" + exit 1 +fi + +if [[ -z ${BACKUP_LOCATION} ]]; then + while [[ -z ${BACKUP_LOCATION} ]]; do + read -ep "Backup location (absolute path, starting with /): " BACKUP_LOCATION + done +fi + +if [[ ! ${BACKUP_LOCATION} =~ ^/ ]]; then + echo "Backup directory needs to be given as absolute path (starting with /)." + exit 1 +fi + +if [[ -f ${BACKUP_LOCATION} ]]; then + echo "${BACKUP_LOCATION} is a file!" + exit 1 +fi + +if [[ ! -d ${BACKUP_LOCATION} ]]; then + echo "${BACKUP_LOCATION} is not a directory" + read -p "Create it now? [y|N] " CREATE_BACKUP_LOCATION + if [[ ! ${CREATE_BACKUP_LOCATION,,} =~ ^(yes|y)$ ]]; then + exit 1 + else + mkdir -p ${BACKUP_LOCATION} + chmod 755 ${BACKUP_LOCATION} + fi +else + if [[ ${1} == "backup" ]] && [[ -z $(echo $(stat -Lc %a ${BACKUP_LOCATION}) | grep -oE '[0-9][0-9][5-7]') ]]; then + echo "${BACKUP_LOCATION} is not write-able for others, that's required for a backup." + exit 1 + fi +fi + +BACKUP_LOCATION=$(echo ${BACKUP_LOCATION} | sed 's#/$##') +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +COMPOSE_FILE=${SCRIPT_DIR}/../docker-compose.yml +ENV_FILE=${SCRIPT_DIR}/../.env +THREADS=$(echo ${THREADS:-1}) +ARCH=$(uname -m) + +if ! [[ "${THREADS}" =~ ^[1-9][0-9]?$ ]] ; then + echo "Thread input is not a number!" + exit 1 +elif [[ "${THREADS}" =~ ^[1-9][0-9]?$ ]] ; then + echo "Using ${THREADS} Thread(s) for this run." + echo "Notice: You can set the Thread count with the THREADS Variable before you run this script." +fi + +if [ ! -f ${COMPOSE_FILE} ]; then + echo "Compose file not found" + exit 1 +fi + +if [ ! -f ${ENV_FILE} ]; then + echo "Environment file not found" + exit 1 +fi + +echo "Using ${BACKUP_LOCATION} as backup/restore location." +echo + +source ${SCRIPT_DIR}/../mailcow.conf + +if [[ -z ${COMPOSE_PROJECT_NAME} ]]; then + echo "Could not determine compose project name" + exit 1 +else + echo "Found project name ${COMPOSE_PROJECT_NAME}" + CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd "[0-9A-Za-z-_]") +fi + +if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then + >&2 echo -e "\e[31mBusyBox grep detected on local system, please install GNU grep\e[0m" + exit 1 +fi + + +function backup() { + DATE=$(date +"%Y-%m-%d-%H-%M-%S") + mkdir -p "${BACKUP_LOCATION}/mailcow-${DATE}" + chmod 755 "${BACKUP_LOCATION}/mailcow-${DATE}" + cp "${SCRIPT_DIR}/../mailcow.conf" "${BACKUP_LOCATION}/mailcow-${DATE}" + touch "${BACKUP_LOCATION}/mailcow-${DATE}/.$ARCH" + for bin in docker; do + if [[ -z $(which ${bin}) ]]; then + >&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m" + exit 1 + fi + done + while (( "$#" )); do + case "$1" in + vmail|all) + docker run --name mailcow-backup --rm \ + -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ + -v $(docker volume ls -qf name=^${CMPS_PRJ}_vmail-vol-1$):/vmail:ro,z \ + ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_vmail.tar.gz /vmail + ;;& + crypt|all) + docker run --name mailcow-backup --rm \ + -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ + -v $(docker volume ls -qf name=^${CMPS_PRJ}_crypt-vol-1$):/crypt:ro,z \ + ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_crypt.tar.gz /crypt + ;;& + redis|all) + docker exec $(docker ps -qf name=redis-mailcow) redis-cli -a ${REDISPASS} --no-auth-warning save + docker run --name mailcow-backup --rm \ + -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ + -v $(docker volume ls -qf name=^${CMPS_PRJ}_redis-vol-1$):/redis:ro,z \ + ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_redis.tar.gz /redis + ;;& + rspamd|all) + docker run --name mailcow-backup --rm \ + -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ + -v $(docker volume ls -qf name=^${CMPS_PRJ}_rspamd-vol-1$):/rspamd:ro,z \ + ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_rspamd.tar.gz /rspamd + ;;& + postfix|all) + docker run --name mailcow-backup --rm \ + -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ + -v $(docker volume ls -qf name=^${CMPS_PRJ}_postfix-vol-1$):/postfix:ro,z \ + ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_postfix.tar.gz /postfix + ;;& + mysql|all) + SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' ${COMPOSE_FILE}) + if [[ -z "${SQLIMAGE}" ]]; then + echo "Could not determine SQL image version, skipping backup..." + shift + continue + else + echo "Using SQL image ${SQLIMAGE}, starting..." + docker run --name mailcow-backup --rm \ + --network $(docker network ls -qf name=^${CMPS_PRJ}_mailcow-network$) \ + -v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/var/lib/mysql/:ro,z \ + -t --entrypoint= \ + --sysctl net.ipv6.conf.all.disable_ipv6=1 \ + -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ + ${SQLIMAGE} /bin/sh -c "mariabackup --host mysql --user root --password ${DBROOT} --backup --rsync --target-dir=/backup_mariadb ; \ + mariabackup --prepare --target-dir=/backup_mariadb ; \ + chown -R 999:999 /backup_mariadb ; \ + /bin/tar --warning='no-file-ignored' --use-compress-program='gzip --rsyncable' -Pcvpf /backup/backup_mariadb.tar.gz /backup_mariadb ;" + fi + ;;& + --delete-days) + shift + if [[ "${1}" =~ ^[0-9]+$ ]]; then + find ${BACKUP_LOCATION}/mailcow-* -maxdepth 0 -mmin +$((${1}*60*24)) -exec rm -rvf {} \; + else + echo "Parameter of --delete-days is not a number." + fi + ;; + esac + shift + done +} + +function restore() { + for bin in docker; do + if [[ -z $(which ${bin}) ]]; then + >&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m" + exit 1 + fi + done + + if [ "${DOCKER_COMPOSE_VERSION}" == "native" ]; then + COMPOSE_COMMAND="docker compose" + + elif [ "${DOCKER_COMPOSE_VERSION}" == "standalone" ]; then + COMPOSE_COMMAND="docker-compose" + + else + echo -e "\e[31mCan not read DOCKER_COMPOSE_VERSION variable from mailcow.conf! Is your mailcow up to date? Exiting...\e[0m" + exit 1 + fi + + echo + echo "Stopping watchdog-mailcow..." + docker stop $(docker ps -qf name=watchdog-mailcow) + echo + RESTORE_LOCATION="${1}" + shift + while (( "$#" )); do + case "$1" in + vmail) + docker stop $(docker ps -qf name=dovecot-mailcow) + docker run -i --name mailcow-backup --rm \ + -v ${RESTORE_LOCATION}:/backup:z \ + -v $(docker volume ls -qf name=^${CMPS_PRJ}_vmail-vol-1$):/vmail:z \ + ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_vmail.tar.gz + docker start $(docker ps -aqf name=dovecot-mailcow) + echo + echo "In most cases it is not required to run a full resync, you can run the command printed below at any time after testing wether the restore process broke a mailbox:" + echo + echo "docker exec $(docker ps -qf name=dovecot-mailcow) doveadm force-resync -A '*'" + echo + read -p "Force a resync now? [y|N] " FORCE_RESYNC + if [[ ${FORCE_RESYNC,,} =~ ^(yes|y)$ ]]; then + docker exec $(docker ps -qf name=dovecot-mailcow) doveadm force-resync -A '*' + else + echo "OK, skipped." + fi + ;; + redis) + docker stop $(docker ps -qf name=redis-mailcow) + docker run -i --name mailcow-backup --rm \ + -v ${RESTORE_LOCATION}:/backup:z \ + -v $(docker volume ls -qf name=^${CMPS_PRJ}_redis-vol-1$):/redis:z \ + ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_redis.tar.gz + docker start $(docker ps -aqf name=redis-mailcow) + ;; + crypt) + docker stop $(docker ps -qf name=dovecot-mailcow) + docker run -i --name mailcow-backup --rm \ + -v ${RESTORE_LOCATION}:/backup:z \ + -v $(docker volume ls -qf name=^${CMPS_PRJ}_crypt-vol-1$):/crypt:z \ + ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_crypt.tar.gz + docker start $(docker ps -aqf name=dovecot-mailcow) + ;; + rspamd) + if [[ $(find "${RESTORE_LOCATION}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') == "" ]]; then + echo -e "\e[33mCould not find a architecture signature of the loaded backup... Maybe the backup was done before the multiarch update?" + sleep 2 + echo -e "Continuing anyhow. If rspamd is crashing opon boot try remove the rspamd volume with docker volume rm ${CMPS_PRJ}_rspamd-vol-1 after you've stopped the stack.\e[0m" + sleep 2 + docker stop $(docker ps -qf name=rspamd-mailcow) + docker run -i --name mailcow-backup --rm \ + -v ${RESTORE_LOCATION}:/backup:z \ + -v $(docker volume ls -qf name=^${CMPS_PRJ}_rspamd-vol-1$):/rspamd:z \ + ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_rspamd.tar.gz + docker start $(docker ps -aqf name=rspamd-mailcow) + elif [[ $ARCH != $(find "${RESTORE_LOCATION}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') ]]; then + echo -e "\e[31mThe Architecture of the backed up mailcow OS is different then your restoring mailcow OS..." + sleep 2 + echo -e "Skipping rspamd due to compatibility issues!\e[0m" + else + docker stop $(docker ps -qf name=rspamd-mailcow) + docker run -i --name mailcow-backup --rm \ + -v ${RESTORE_LOCATION}:/backup:z \ + -v $(docker volume ls -qf name=^${CMPS_PRJ}_rspamd-vol-1$):/rspamd:z \ + ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_rspamd.tar.gz + docker start $(docker ps -aqf name=rspamd-mailcow) + fi + ;; + postfix) + docker stop $(docker ps -qf name=postfix-mailcow) + docker run -i --name mailcow-backup --rm \ + -v ${RESTORE_LOCATION}:/backup:z \ + -v $(docker volume ls -qf name=^${CMPS_PRJ}_postfix-vol-1$):/postfix:z \ + ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_postfix.tar.gz + docker start $(docker ps -aqf name=postfix-mailcow) + ;; + mysql|mariadb) + SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' ${COMPOSE_FILE}) + if [[ -z "${SQLIMAGE}" ]]; then + echo "Could not determine SQL image version, skipping restore..." + shift + continue + elif [ ! -f "${RESTORE_LOCATION}/mailcow.conf" ]; then + echo "Could not find the corresponding mailcow.conf in ${RESTORE_LOCATION}, skipping restore." + echo "If you lost that file, copy the last working mailcow.conf file to ${RESTORE_LOCATION} and restart the restore process." + shift + continue + else + read -p "mailcow will be stopped and the currently active mailcow.conf will be modified to use the DB parameters found in ${RESTORE_LOCATION}/mailcow.conf - do you want to proceed? [Y|n] " MYSQL_STOP_MAILCOW + if [[ ${MYSQL_STOP_MAILCOW,,} =~ ^(no|n|N)$ ]]; then + echo "OK, skipped." + shift + continue + else + echo "Stopping mailcow..." + ${COMPOSE_COMMAND} -f ${COMPOSE_FILE} --env-file ${ENV_FILE} down + fi + #docker stop $(docker ps -qf name=mysql-mailcow) + if [[ -d "${RESTORE_LOCATION}/mysql" ]]; then + docker run --name mailcow-backup --rm \ + -v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/var/lib/mysql/:rw,z \ + --entrypoint= \ + -v ${RESTORE_LOCATION}/mysql:/backup:z \ + ${SQLIMAGE} /bin/bash -c "shopt -s dotglob ; /bin/rm -rf /var/lib/mysql/* ; rsync -avh --usermap=root:mysql --groupmap=root:mysql /backup/ /var/lib/mysql/" + elif [[ -f "${RESTORE_LOCATION}/backup_mysql.gz" ]]; then + docker run \ + -i --name mailcow-backup --rm \ + -v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/var/lib/mysql/:z \ + --entrypoint= \ + -u mysql \ + -v ${RESTORE_LOCATION}:/backup:z \ + ${SQLIMAGE} /bin/sh -c "mysqld --skip-grant-tables & \ + until mysqladmin ping; do sleep 3; done && \ + echo Restoring... && \ + gunzip < backup/backup_mysql.gz | mysql -uroot && \ + mysql -uroot -e SHUTDOWN;" + elif [[ -f "${RESTORE_LOCATION}/backup_mariadb.tar.gz" ]]; then + docker run --name mailcow-backup --rm \ + -v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/backup_mariadb/:rw,z \ + --entrypoint= \ + -v ${RESTORE_LOCATION}:/backup:z \ + ${SQLIMAGE} /bin/bash -c "shopt -s dotglob ; \ + /bin/rm -rf /backup_mariadb/* ; \ + /bin/tar -Pxvzf /backup/backup_mariadb.tar.gz" + fi + echo "Modifying mailcow.conf..." + source ${RESTORE_LOCATION}/mailcow.conf + sed -i --follow-symlinks "/DBNAME/c\DBNAME=${DBNAME}" ${SCRIPT_DIR}/../mailcow.conf + sed -i --follow-symlinks "/DBUSER/c\DBUSER=${DBUSER}" ${SCRIPT_DIR}/../mailcow.conf + sed -i --follow-symlinks "/DBPASS/c\DBPASS=${DBPASS}" ${SCRIPT_DIR}/../mailcow.conf + sed -i --follow-symlinks "/DBROOT/c\DBROOT=${DBROOT}" ${SCRIPT_DIR}/../mailcow.conf + source ${SCRIPT_DIR}/../mailcow.conf + echo "Starting mailcow..." + ${COMPOSE_COMMAND} -f ${COMPOSE_FILE} --env-file ${ENV_FILE} up -d + #docker start $(docker ps -aqf name=mysql-mailcow) + fi + ;; + esac + shift + done + echo + echo "Starting watchdog-mailcow..." + docker start $(docker ps -aqf name=watchdog-mailcow) +} + +if [[ ${1} == "backup" ]]; then + backup ${@,,} +elif [[ ${1} == "restore" ]]; then + i=1 + declare -A FOLDER_SELECTION + if [[ $(find ${BACKUP_LOCATION}/mailcow-* -maxdepth 1 -type d 2> /dev/null| wc -l) -lt 1 ]]; then + echo "Selected backup location has no subfolders" + exit 1 + fi + for folder in $(ls -d ${BACKUP_LOCATION}/mailcow-*/); do + echo "[ ${i} ] - ${folder}" + FOLDER_SELECTION[${i}]="${folder}" + ((i++)) + done + echo + input_sel=0 + while [[ ${input_sel} -lt 1 || ${input_sel} -gt ${i} ]]; do + read -p "Select a restore point: " input_sel + done + i=1 + echo + declare -A FILE_SELECTION + RESTORE_POINT="${FOLDER_SELECTION[${input_sel}]}" + if [[ -z $(find "${FOLDER_SELECTION[${input_sel}]}" -maxdepth 1 \( -type d -o -type f \) -regex ".*\(redis\|rspamd\|mariadb\|mysql\|crypt\|vmail\|postfix\).*") ]]; then + echo "No datasets found" + exit 1 + fi + + echo "[ 0 ] - all" + # find all files in folder with *.gz extension, print their base names, remove backup_, remove .tar (if present), remove .gz + FILE_SELECTION[0]=$(find "${FOLDER_SELECTION[${input_sel}]}" -maxdepth 1 \( -type d -o -type f \) \( -name '*.gz' -o -name 'mysql' \) -printf '%f\n' | sed 's/backup_*//' | sed 's/\.[^.]*$//' | sed 's/\.[^.]*$//') + for file in $(ls -f "${FOLDER_SELECTION[${input_sel}]}"); do + if [[ ${file} =~ vmail ]]; then + echo "[ ${i} ] - Mail directory (/var/vmail)" + FILE_SELECTION[${i}]="vmail" + ((i++)) + elif [[ ${file} =~ crypt ]]; then + echo "[ ${i} ] - Crypt data" + FILE_SELECTION[${i}]="crypt" + ((i++)) + elif [[ ${file} =~ redis ]]; then + echo "[ ${i} ] - Redis DB" + FILE_SELECTION[${i}]="redis" + ((i++)) + elif [[ ${file} =~ rspamd ]]; then + if [[ $(find "${FOLDER_SELECTION[${input_sel}]}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') == "" ]]; then + echo "[ ${i} ] - Rspamd data (unkown Arch detected, restore with caution!)" + FILE_SELECTION[${i}]="rspamd" + ((i++)) + elif [[ $ARCH != $(find "${FOLDER_SELECTION[${input_sel}]}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') ]]; then + echo -e "\e[31m[ NaN ] - Rspamd data (incompatible Arch, cannot restore it)\e[0m" + else + echo "[ ${i} ] - Rspamd data" + FILE_SELECTION[${i}]="rspamd" + ((i++)) + fi + elif [[ ${file} =~ postfix ]]; then + echo "[ ${i} ] - Postfix data" + FILE_SELECTION[${i}]="postfix" + ((i++)) + elif [[ ${file} =~ mysql ]] || [[ ${file} =~ mariadb ]]; then + echo "[ ${i} ] - SQL DB" + FILE_SELECTION[${i}]="mysql" + ((i++)) + fi + done + echo + input_sel=-1 + while [[ ${input_sel} -lt 0 || ${input_sel} -gt ${i} ]]; do + read -p "Select a dataset to restore: " input_sel + done + echo "Restoring ${FILE_SELECTION[${input_sel}]} from ${RESTORE_POINT}..." + restore "${RESTORE_POINT}" ${FILE_SELECTION[${input_sel}]} +fi diff --git a/mailcow/helper-scripts/check_translations.rb b/mailcow/helper-scripts/check_translations.rb new file mode 100755 index 0000000..a8bfb5a --- /dev/null +++ b/mailcow/helper-scripts/check_translations.rb @@ -0,0 +1,34 @@ +#!/usr/bin/env ruby + +MASTER="en-gb" + +DIR = "#{__dir__}/.." + +keys = %x[sed -r 's/.*(\\['.*'\\]\\['.*'\\]).*/\\1/g' #{DIR}/data/web/lang/lang.#{MASTER}.php | grep '^\\\[' | sed 's/\\[/\\\\[/g' | sed 's/\\]/\\\\]/g'|sort | uniq] + +not_used_in_php = [] +keys.split("\n").each do |key| + %x[git grep "#{key}" -- #{DIR}/data/web/*.php #{DIR}/data/web/inc #{DIR}/data/web/modals] + if $?.exitstatus > 0 + not_used_in_php << key + end +end + +# \['user'\]\['username'\] +# \['user'\]\['waiting'\] +# \['warning'\]\['spam_alias_temp_error'\] + +not_used = [] +not_used_in_php.each do |string| + section = string.scan(/([a-z]+)/)[0][0] + key = string.scan(/([a-z]+)/)[1][0] + %x[git grep lang.#{key} -- #{DIR}/data/web/js/#{section}.js #{DIR}/data/web/js/debug.js] + if $?.exitstatus > 0 + not_used << string + end +end + +puts "# Remove unused translation keys:" +not_used.each do |key| + puts "sed -i \"/\\$lang#{key}.*;/d\" data/web/lang/lang.??.php" +end diff --git a/mailcow/helper-scripts/expiry-dates.sh b/mailcow/helper-scripts/expiry-dates.sh new file mode 100755 index 0000000..ec2a63a --- /dev/null +++ b/mailcow/helper-scripts/expiry-dates.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +[[ -f mailcow.conf ]] && source mailcow.conf +[[ -f ../mailcow.conf ]] && source ../mailcow.conf + +POSTFIX=$(echo | openssl s_client -connect ${MAILCOW_HOSTNAME}:${SMTP_PORT} -starttls smtp 2>/dev/null | openssl x509 -inform pem -noout -enddate | cut -d "=" -f 2) +DOVECOT=$(echo | openssl s_client -connect ${MAILCOW_HOSTNAME}:${IMAP_PORT} -starttls imap 2>/dev/null | openssl x509 -inform pem -noout -enddate | cut -d "=" -f 2) +NGINX=$(echo | openssl s_client -connect ${MAILCOW_HOSTNAME}:${HTTPS_PORT} 2>/dev/null | openssl x509 -inform pem -noout -enddate | cut -d "=" -f 2) + +echo "TLS expiry dates:" +echo "Postfix: ${POSTFIX}" +echo "Dovecot: ${DOVECOT}" +echo "Nginx: ${NGINX}" diff --git a/mailcow/helper-scripts/generate_caa_record.py b/mailcow/helper-scripts/generate_caa_record.py new file mode 100755 index 0000000..5ccfbd0 --- /dev/null +++ b/mailcow/helper-scripts/generate_caa_record.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +# Based on github.com/diafygi/acme-tiny, original copyright: +# Copyright Daniel Roesler, under MIT license, see LICENSE at github.com/diafygi/acme-tiny +import argparse, subprocess, json, os, sys, base64, binascii, time, hashlib, re, copy, textwrap, logging +try: + from urllib.request import urlopen, Request # Python 3 +except ImportError: # pragma: no cover + from urllib2 import urlopen, Request # Python 2 + +DEFAULT_DIRECTORY_URL = "https://acme-v02.api.letsencrypt.org/directory" + +LOGGER = logging.getLogger(__name__) +LOGGER.addHandler(logging.StreamHandler()) +LOGGER.setLevel(logging.INFO) + +def get_id(account_key, log=LOGGER, directory_url=DEFAULT_DIRECTORY_URL, contact=None): + directory, acct_headers, alg, jwk = None, None, None, None # global variables + + # helper functions - base64 encode for jose spec + def _b64(b): + return base64.urlsafe_b64encode(b).decode('utf8').replace("=", "") + + # helper function - run external commands + def _cmd(cmd_list, stdin=None, cmd_input=None, err_msg="Command Line Error"): + proc = subprocess.Popen(cmd_list, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = proc.communicate(cmd_input) + if proc.returncode != 0: + raise IOError("{0}\n{1}".format(err_msg, err)) + return out + + # helper function - make request and automatically parse json response + def _do_request(url, data=None, err_msg="Error", depth=0): + try: + resp = urlopen(Request(url, data=data, headers={"Content-Type": "application/jose+json", "User-Agent": "acme-tiny"})) + resp_data, code, headers = resp.read().decode("utf8"), resp.getcode(), resp.headers + except IOError as e: + resp_data = e.read().decode("utf8") if hasattr(e, "read") else str(e) + code, headers = getattr(e, "code", None), {} + try: + resp_data = json.loads(resp_data) # try to parse json results + except ValueError: + pass # ignore json parsing errors + if depth < 100 and code == 400 and resp_data['type'] == "urn:ietf:params:acme:error:badNonce": + raise IndexError(resp_data) # allow 100 retrys for bad nonces + if code not in [200, 201, 204]: + raise ValueError("{0}:\nUrl: {1}\nData: {2}\nResponse Code: {3}\nResponse: {4}".format(err_msg, url, data, code, resp_data)) + return resp_data, code, headers + + # helper function - make signed requests + def _send_signed_request(url, payload, err_msg, depth=0): + payload64 = "" if payload is None else _b64(json.dumps(payload).encode('utf8')) + new_nonce = _do_request(directory['newNonce'])[2]['Replay-Nonce'] + protected = {"url": url, "alg": alg, "nonce": new_nonce} + protected.update({"jwk": jwk} if acct_headers is None else {"kid": acct_headers['Location']}) + protected64 = _b64(json.dumps(protected).encode('utf8')) + protected_input = "{0}.{1}".format(protected64, payload64).encode('utf8') + out = _cmd(["openssl", "dgst", "-sha256", "-sign", account_key], stdin=subprocess.PIPE, cmd_input=protected_input, err_msg="OpenSSL Error") + data = json.dumps({"protected": protected64, "payload": payload64, "signature": _b64(out)}) + try: + return _do_request(url, data=data.encode('utf8'), err_msg=err_msg, depth=depth) + except IndexError: # retry bad nonces (they raise IndexError) + return _send_signed_request(url, payload, err_msg, depth=(depth + 1)) + + # helper function - poll until complete + def _poll_until_not(url, pending_statuses, err_msg): + result, t0 = None, time.time() + while result is None or result['status'] in pending_statuses: + assert (time.time() - t0 < 3600), "Polling timeout" # 1 hour timeout + time.sleep(0 if result is None else 2) + result, _, _ = _send_signed_request(url, None, err_msg) + return result + + # parse account key to get public key + log.info("Parsing account key...") + out = _cmd(["openssl", "rsa", "-in", account_key, "-noout", "-text"], err_msg="OpenSSL Error") + pub_pattern = r"modulus:[\s]+?00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)" + pub_hex, pub_exp = re.search(pub_pattern, out.decode('utf8'), re.MULTILINE|re.DOTALL).groups() + pub_exp = "{0:x}".format(int(pub_exp)) + pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp + alg, jwk = "RS256", { + "e": _b64(binascii.unhexlify(pub_exp.encode("utf-8"))), + "kty": "RSA", + "n": _b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))), + } + accountkey_json = json.dumps(jwk, sort_keys=True, separators=(',', ':')) + thumbprint = _b64(hashlib.sha256(accountkey_json.encode('utf8')).digest()) + + # get the ACME directory of urls + log.info("Getting directory...") + directory, _, _ = _do_request(directory_url, err_msg="Error getting directory") + log.info("Directory found!") + + # create account and get the global key identifier + log.info("Registering account...") + reg_payload = {"termsOfServiceAgreed": True} if contact is None else {"termsOfServiceAgreed": True, "contact": contact} + account, code, acct_headers = _send_signed_request(directory['newAccount'], reg_payload, "Error registering") + log.info("Registered!" if code == 201 else "Already registered!") + + return acct_headers['Location'] + +def main(argv=None): + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=textwrap.dedent("""\ + Generate a CAA record for Mailcow. + + Example Usage: python mailcow_gencaa.py --account-key data/assets/ssl/acme/account.pem + """) + ) + parser.add_argument("--account-key", required=True, help="path to your Let's Encrypt account private key") + parser.add_argument("--quiet", action="store_const", const=logging.ERROR, help="suppress output except for errors") + parser.add_argument("--directory-url", default=DEFAULT_DIRECTORY_URL, help="certificate authority directory url, default is Let's Encrypt") + parser.add_argument("--contact", metavar="CONTACT", default=None, nargs="*", help="Contact details (e.g. mailto:aaa@bbb.com) for your account-key") + + args = parser.parse_args(argv) + LOGGER.setLevel(args.quiet or LOGGER.level) + id = get_id(args.account_key, log=LOGGER, directory_url=args.directory_url, contact=args.contact) + print("Use this as your CAA record:") + print('issue 128 "letsencrypt.org;accounturi={}"'.format(id)) + +if __name__ == "__main__": # pragma: no cover + main(sys.argv[1:]) diff --git a/mailcow/helper-scripts/mailcow-reset-admin.sh b/mailcow/helper-scripts/mailcow-reset-admin.sh new file mode 100755 index 0000000..ea8a4a4 --- /dev/null +++ b/mailcow/helper-scripts/mailcow-reset-admin.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +[[ -f mailcow.conf ]] && source mailcow.conf +[[ -f ../mailcow.conf ]] && source ../mailcow.conf + +if [[ -z ${DBUSER} ]] || [[ -z ${DBPASS} ]] || [[ -z ${DBNAME} ]]; then + echo "Cannot find mailcow.conf, make sure this script is run from within the mailcow folder." + exit 1 +fi + +echo -n "Checking MySQL service... " +if [[ -z $(docker ps -qf name=mysql-mailcow) ]]; then + echo "failed" + echo "MySQL (mysql-mailcow) is not up and running, exiting..." + exit 1 +fi + +echo "OK" +read -r -p "Are you sure you want to reset the mailcow administrator account? [y/N] " response +response=${response,,} # tolower +if [[ "$response" =~ ^(yes|y)$ ]]; then + echo -e "\nWorking, please wait..." + random=$( /dev/null | head -c${1:-16}) + password=$(docker exec -it $(docker ps -qf name=dovecot-mailcow) doveadm pw -s SSHA256 -p ${random} | tr -d '\r') + docker exec -it $(docker ps -qf name=mysql-mailcow) mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -e "DELETE FROM admin WHERE username='admin';" + docker exec -it $(docker ps -qf name=mysql-mailcow) mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -e "DELETE FROM domain_admins WHERE username='admin';" + docker exec -it $(docker ps -qf name=mysql-mailcow) mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -e "INSERT INTO admin (username, password, superadmin, active) VALUES ('admin', '${password}', 1, 1);" + docker exec -it $(docker ps -qf name=mysql-mailcow) mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -e "DELETE FROM tfa WHERE username='admin';" + echo " +Reset credentials: +--- +Username: admin +Password: ${random} +TFA: none +" +else + echo "Operation canceled." +fi diff --git a/mailcow/helper-scripts/reset-learns.sh b/mailcow/helper-scripts/reset-learns.sh new file mode 100755 index 0000000..b6723f5 --- /dev/null +++ b/mailcow/helper-scripts/reset-learns.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +read -r -p "Are you sure you want to reset learned hashes from Rspamd (fuzzy, bayes, neural)? [y/N] " response +response=${response,,} # tolower +if [[ "$response" =~ ^(yes|y)$ ]]; then + echo "Working, please wait..." + REDIS_ID=$(docker ps -qf name=redis-mailcow) + RSPAMD_ID=$(docker ps -qf name=rspamd-mailcow) + + if [ -z ${REDIS_ID} ] || [ -z ${RSPAMD_ID} ]; then + echo "Cannot determine Redis or Rspamd container ID" + exit 1 + else + echo "Stopping Rspamd container" + docker stop ${RSPAMD_ID} + echo "LUA will return nil when it succeeds or print a warning/error when it fails." + echo "Deleting all RS* keys - if any" + docker exec -it ${REDIS_ID} redis-cli -a ${REDISPASS} --no-auth-warning EVAL "for _,k in ipairs(redis.call('keys', ARGV[1])) do redis.call('del', k) end" 0 'RS*' + echo "Deleting all BAYES* keys - if any" + docker exec -it ${REDIS_ID} redis-cli -a ${REDISPASS} --no-auth-warning EVAL "for _,k in ipairs(redis.call('keys', ARGV[1])) do redis.call('del', k) end" 0 'BAYES*' + echo "Deleting all learned* keys - if any" + docker exec -it ${REDIS_ID} redis-cli -a ${REDISPASS} --no-auth-warning EVAL "for _,k in ipairs(redis.call('keys', ARGV[1])) do redis.call('del', k) end" 0 'learned*' + echo "Deleting all fuzzy* keys - if any" + docker exec -it ${REDIS_ID} redis-cli -a ${REDISPASS} --no-auth-warning EVAL "for _,k in ipairs(redis.call('keys', ARGV[1])) do redis.call('del', k) end" 0 'fuzzy*' + echo "Deleting all tRFANN* keys - if any" + docker exec -it ${REDIS_ID} redis-cli -a ${REDISPASS} --no-auth-warning EVAL "for _,k in ipairs(redis.call('keys', ARGV[1])) do redis.call('del', k) end" 0 'tRFANN*' + echo "Starting Rspamd container" + docker start ${RSPAMD_ID} + fi +fi diff --git a/mailcow/helper-scripts/update_compose.sh b/mailcow/helper-scripts/update_compose.sh new file mode 100755 index 0000000..ae2d6ab --- /dev/null +++ b/mailcow/helper-scripts/update_compose.sh @@ -0,0 +1,72 @@ +#!/bin/bash +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +source ${SCRIPT_DIR}/../mailcow.conf + +if [ "${DOCKER_COMPOSE_VERSION}" == "standalone" ]; then +LATEST_COMPOSE=$(curl -Ls -w %{url_effective} -o /dev/null https://github.com/docker/compose/releases/latest) # redirect to latest release +LATEST_COMPOSE=${LATEST_COMPOSE##*/v} #get the latest version from the redirect, excluding the "v" prefix +COMPOSE_VERSION=$(docker-compose version --short) +if [[ "$LATEST_COMPOSE" != "$COMPOSE_VERSION" ]]; then + echo -e "\e[33mA new docker-compose Version is available: $LATEST_COMPOSE\e[0m" + echo -e "\e[33mYour Version is: $COMPOSE_VERSION\e[0m" +else + echo -e "\e[32mYour docker-compose Version is up to date! Not updating it...\e[0m" + exit 0 +fi +read -r -p "Do you want to update your docker-compose Version? It will automatic upgrade your docker-compose installation (recommended)? [y/N] " updatecomposeresponse + if [[ ! "${updatecomposeresponse}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + echo "OK, not updating docker-compose." + exit 0 + fi +echo -e "\e[32mFetching new docker-compose (standalone) version...\e[0m" +echo -e "\e[32mTrying to determine GLIBC version...\e[0m" + if ldd --version > /dev/null; then + GLIBC_V=$(ldd --version | grep -E '(GLIBC|GNU libc)' | rev | cut -d ' ' -f1 | rev | cut -d '.' -f2) + if [ ! -z "${GLIBC_V}" ] && [ ${GLIBC_V} -gt 27 ]; then + DC_DL_SUFFIX= + else + DC_DL_SUFFIX=legacy + fi + else + DC_DL_SUFFIX=legacy + fi + sleep 1 + if [[ $(command -v pip 2>&1) && $(pip list --local 2>&1 | grep -v DEPRECATION | grep -c docker-compose) == 1 || $(command -v pip3 2>&1) && $(pip3 list --local 2>&1 | grep -v DEPRECATION | grep -c docker-compose) == 1 ]]; then + echo -e "\e[33mFound a docker-compose Version installed with pip!\e[0m" + echo -e "\e[31mPlease uninstall the pip Version of docker-compose since it doesn't support Versions higher than 1.29.2.\e[0m" + sleep 2 + echo -e "\e[33mExiting...\e[0m" + exit 1 + #prevent breaking a working docker-compose installed with pip + elif [[ $(curl -sL -w "%{http_code}" https://github.com/docker/compose/releases/latest -o /dev/null) == "200" ]]; then + LATEST_COMPOSE=$(curl -Ls -w %{url_effective} -o /dev/null https://github.com/docker/compose/releases/latest) # redirect to latest release + LATEST_COMPOSE=${LATEST_COMPOSE##*/} #get the latest version from the redirect, inlcuding the "v" prefix + COMPOSE_VERSION=$(docker-compose version --short) + if [[ "$LATEST_COMPOSE" != "$COMPOSE_VERSION" ]]; then + COMPOSE_PATH=$(command -v docker-compose) + if [[ -w ${COMPOSE_PATH} ]]; then + curl -#L https://github.com/docker/compose/releases/download/${LATEST_COMPOSE}/docker-compose-$(uname -s)-$(uname -m) > $COMPOSE_PATH + chmod +x $COMPOSE_PATH + echo -e "\e[32mYour Docker Compose (standalone) has been updated to: $LATEST_COMPOSE\e[0m" + exit 0 + else + echo -e "\e[33mWARNING: $COMPOSE_PATH is not writable, but new version $LATEST_COMPOSE is available (installed: $COMPOSE_VERSION)\e[0m" + exit 1 + fi + fi + else + echo -e "\e[33mCannot determine latest docker-compose version, skipping...\e[0m" + exit 1 + fi + +elif [ "${DOCKER_COMPOSE_VERSION}" == "native" ]; then + echo -e "\e[31mYou are using the native Docker Compose Plugin. This Script is for the standalone Docker Compose Version only.\e[0m" + sleep 2 + echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m" + exit 1 + +else + echo -e "\e[31mCan not read DOCKER_COMPOSE_VERSION variable from mailcow.conf! Is your mailcow up to date? Exiting...\e[0m" + exit 1 +fi \ No newline at end of file diff --git a/mailcow/helper-scripts/update_postscreen_whitelist.sh b/mailcow/helper-scripts/update_postscreen_whitelist.sh new file mode 100644 index 0000000..04335bd --- /dev/null +++ b/mailcow/helper-scripts/update_postscreen_whitelist.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )" +WORKING_DIR=${SCRIPT_DIR}/postwhite_tmp +SPFTOOLS_DIR=${WORKING_DIR}/spf-tools +POSTWHITE_DIR=${WORKING_DIR}/postwhite +POSTWHITE_CONF=${POSTWHITE_DIR}/postwhite.conf + +CUSTOM_HOSTS='"web.de gmx.net mail.de freenet.de arcor.de unity-mail.de"' +STATIC_HOSTS=( + "194.25.134.0/24 permit # t-online.de" +) + +mkdir ${SCRIPT_DIR}/postwhite_tmp +git clone https://github.com/spf-tools/spf-tools.git ${SPFTOOLS_DIR} +git clone https://github.com/stevejenkins/postwhite.git ${POSTWHITE_DIR} + +function set_config() { + sudo sed -i "s@^\($1\s*=\s*\).*\$@\1$2@" ${POSTWHITE_CONF} +} + +set_config custom_hosts "${CUSTOM_HOSTS}" +set_config reload_postfix no +set_config postfixpath /. +set_config spftoolspath ${WORKING_DIR}/spf-tools +set_config whitelist .${SCRIPT_DIR}/../data/conf/postfix/postscreen_access.cidr +set_config yahoo_static_hosts ${POSTWHITE_DIR}/yahoo_static_hosts.txt + +#Fix URL for Yahoo!: https://github.com/stevejenkins/postwhite/issues/59 +sudo sed -i \ + -e 's#yahoo_url="https://help.yahoo.com/kb/SLN23997.html"#yahoo_url="https://senders.yahooinc.com/outbound-mail-servers/"#' \ + -e 's#echo "ipv6:$line";#echo "ipv6:$line" | grep -v "ipv6:::";#' \ + -e 's#`command -v wget`#`command -v skip-wget`#' \ + ${POSTWHITE_DIR}/scrape_yahoo + +cd ${POSTWHITE_DIR} +./postwhite ${POSTWHITE_CONF} + +( IFS=$'\n'; echo "${STATIC_HOSTS[*]}" >> "${SCRIPT_DIR}/../data/conf/postfix/postscreen_access.cidr") + +rm -r ${WORKING_DIR} \ No newline at end of file diff --git a/mailcow/update.sh b/mailcow/update.sh new file mode 100755 index 0000000..ea2fcf1 --- /dev/null +++ b/mailcow/update.sh @@ -0,0 +1,1634 @@ +#!/usr/bin/env bash + +############## Begin Function Section ############## + +check_online_status() { + CHECK_ONLINE_DOMAINS=('https://github.com' 'https://hub.docker.com') + for domain in "${CHECK_ONLINE_DOMAINS[@]}"; do + if timeout 6 curl --head --silent --output /dev/null ${domain}; then + return 0 + fi + done + return 1 +} + +prefetch_images() { + [[ -z ${BRANCH} ]] && { echo -e "\e[33m\nUnknown branch...\e[0m"; exit 1; } + git fetch origin #${BRANCH} + while read image; do + if [[ "${image}" == "robbertkl/ipv6nat" ]]; then + if ! grep -qi "ipv6nat-mailcow" docker-compose.yml || grep -qi "enable_ipv6: false" docker-compose.yml; then + continue + fi + fi + RET_C=0 + until docker pull "${image}"; do + RET_C=$((RET_C + 1)) + echo -e "\e[33m\nError pulling $image, retrying...\e[0m" + [ ${RET_C} -gt 3 ] && { echo -e "\e[31m\nToo many failed retries, exiting\e[0m"; exit 1; } + sleep 1 + done + done < <(git show "origin/${BRANCH}:docker-compose.yml" | grep "image:" | awk '{ gsub("image:","", $3); print $2 }') +} + +docker_garbage() { + SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + IMGS_TO_DELETE=() + + declare -A IMAGES_INFO + COMPOSE_IMAGES=($(grep -oP "image: \K(ghcr\.io/)?mailcow.+" "${SCRIPT_DIR}/docker-compose.yml")) + + for existing_image in $(docker images --format "{{.ID}}:{{.Repository}}:{{.Tag}}" | grep -E '(mailcow/|ghcr\.io/mailcow/)'); do + ID=$(echo "$existing_image" | cut -d ':' -f 1) + REPOSITORY=$(echo "$existing_image" | cut -d ':' -f 2) + TAG=$(echo "$existing_image" | cut -d ':' -f 3) + + if [[ "$REPOSITORY" == "mailcow/backup" || "$REPOSITORY" == "ghcr.io/mailcow/backup" ]]; then + if [[ "$TAG" != "" ]]; then + continue + fi + fi + + if [[ " ${COMPOSE_IMAGES[@]} " =~ " ${REPOSITORY}:${TAG} " ]]; then + continue + else + IMGS_TO_DELETE+=("$ID") + IMAGES_INFO["$ID"]="$REPOSITORY:$TAG" + fi + done + + if [[ ! -z ${IMGS_TO_DELETE[*]} ]]; then + echo "The following unused mailcow images were found:" + for id in "${IMGS_TO_DELETE[@]}"; do + echo " ${IMAGES_INFO[$id]} ($id)" + done + + if [ -z "$FORCE" ]; then + read -r -p "Do you want to delete them to free up some space? [y/N] " response + if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + docker rmi ${IMGS_TO_DELETE[*]} + else + echo "OK, skipped." + fi + else + echo "Running in forced mode! Force removing old mailcow images..." + docker rmi ${IMGS_TO_DELETE[*]} + fi + echo -e "\e[32mFurther cleanup...\e[0m" + echo "If you want to cleanup further garbage collected by Docker, please make sure all containers are up and running before cleaning your system by executing \"docker system prune\"" + fi +} + +in_array() { + local e match="$1" + shift + for e; do [[ "$e" == "$match" ]] && return 0; done + return 1 +} + +migrate_docker_nat() { + NAT_CONFIG='{"ipv6":true,"fixed-cidr-v6":"fd00:dead:beef:c0::/80","experimental":true,"ip6tables":true}' + # Min Docker version + DOCKERV_REQ=20.10.2 + # Current Docker version + DOCKERV_CUR=$(docker version -f '{{.Server.Version}}') + if grep -qi "ipv6nat-mailcow" docker-compose.yml && grep -qi "enable_ipv6: true" docker-compose.yml; then + echo -e "\e[32mNative IPv6 implementation available.\e[0m" + echo "This will enable experimental features in the Docker daemon and configure Docker to do the IPv6 NATing instead of ipv6nat-mailcow." + echo '!!! This step is recommended !!!' + echo "mailcow will try to roll back the changes if starting Docker fails after modifying the daemon.json configuration file." + read -r -p "Should we try to enable the native IPv6 implementation in Docker now (recommended)? [y/N] " dockernatresponse + if [[ ! "${dockernatresponse}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + echo "OK, skipping this step." + return 0 + fi + fi + # Sort versions and check if we are running a newer or equal version to req + if [ $(printf "${DOCKERV_REQ}\n${DOCKERV_CUR}" | sort -V | tail -n1) == "${DOCKERV_CUR}" ]; then + # If Dockerd daemon json exists + if [ -s /etc/docker/daemon.json ]; then + IFS=',' read -r -a dockerconfig <<< $(cat /etc/docker/daemon.json | tr -cd '[:alnum:],') + if ! in_array ipv6true "${dockerconfig[@]}" || \ + ! in_array experimentaltrue "${dockerconfig[@]}" || \ + ! in_array ip6tablestrue "${dockerconfig[@]}" || \ + ! grep -qi "fixed-cidr-v6" /etc/docker/daemon.json; then + echo -e "\e[33mWarning:\e[0m You seem to have modified the /etc/docker/daemon.json configuration by yourself and not fully/correctly activated the native IPv6 NAT implementation." + echo "You will need to merge your existing configuration manually or fix/delete the existing daemon.json configuration before trying the update process again." + echo -e "Please merge the following content and restart the Docker daemon:\n" + echo "${NAT_CONFIG}" + return 1 + fi + else + echo "Working on IPv6 NAT, please wait..." + echo "${NAT_CONFIG}" > /etc/docker/daemon.json + ip6tables -F -t nat + [[ -e /etc/rc.conf ]] && rc-service docker restart || systemctl restart docker.service + if [[ $? -ne 0 ]]; then + echo -e "\e[31mError:\e[0m Failed to activate IPv6 NAT! Reverting and exiting." + rm /etc/docker/daemon.json + if [[ -e /etc/rc.conf ]]; then + rc-service docker restart + else + systemctl reset-failed docker.service + systemctl restart docker.service + fi + return 1 + fi + fi + # Removing legacy container + sed -i '/ipv6nat-mailcow:$/,/^$/d' docker-compose.yml + if [ -s docker-compose.override.yml ]; then + sed -i '/ipv6nat-mailcow:$/,/^$/d' docker-compose.override.yml + if [[ "$(cat docker-compose.override.yml | sed '/^\s*$/d' | wc -l)" == "2" ]]; then + mv docker-compose.override.yml docker-compose.override.yml_backup + fi + fi + echo -e "\e[32mGreat! \e[0mNative IPv6 NAT is active.\e[0m" + else + echo -e "\e[31mPlease upgrade Docker to version ${DOCKERV_REQ} or above.\e[0m" + return 0 + fi +} + +remove_obsolete_nginx_ports() { + # Removing obsolete docker-compose.override.yml + for override in docker-compose.override.yml docker-compose.override.yaml; do + if [ -s $override ] ; then + if cat $override | grep nginx-mailcow > /dev/null 2>&1; then + if cat $override | grep -E '(\[::])' > /dev/null 2>&1; then + if cat $override | grep -w 80:80 > /dev/null 2>&1 && cat $override | grep -w 443:443 > /dev/null 2>&1 ; then + echo -e "\e[33mBacking up ${override} to preserve custom changes...\e[0m" + echo -e "\e[33m!!! Manual Merge needed (if other overrides are set) !!!\e[0m" + sleep 3 + cp $override ${override}_backup + sed -i '/nginx-mailcow:$/,/^$/d' $override + echo -e "\e[33mRemoved obsolete NGINX IPv6 Bind from original override File.\e[0m" + if [[ "$(cat $override | sed '/^\s*$/d' | wc -l)" == "2" ]]; then + mv $override ${override}_empty + echo -e "\e[31m${override} is empty. Renamed it to ensure mailcow is startable.\e[0m" + fi + fi + fi + fi + fi + done +} + +detect_docker_compose_command(){ +if ! [[ "${DOCKER_COMPOSE_VERSION}" =~ ^(native|standalone)$ ]]; then + if docker compose > /dev/null 2>&1; then + if docker compose version --short | grep -e "^2." -e "^v2." > /dev/null 2>&1; then + DOCKER_COMPOSE_VERSION=native + COMPOSE_COMMAND="docker compose" + echo -e "\e[33mFound Docker Compose Plugin (native).\e[0m" + echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m" + sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=native/' "$SCRIPT_DIR/mailcow.conf" + sleep 2 + echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m" + else + echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" + echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m" + exit 1 + fi + elif docker-compose > /dev/null 2>&1; then + if ! [[ $(alias docker-compose 2> /dev/null) ]] ; then + if docker-compose version --short | grep "^2." > /dev/null 2>&1; then + DOCKER_COMPOSE_VERSION=standalone + COMPOSE_COMMAND="docker-compose" + echo -e "\e[33mFound Docker Compose Standalone.\e[0m" + echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to standalone\e[0m" + sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=standalone/' "$SCRIPT_DIR/mailcow.conf" + sleep 2 + echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.\e[0m" + else + echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" + echo -e "\e[31mPlease update/install regarding to this doc site: https://docs.mailcow.email/install/\e[0m" + exit 1 + fi + fi + + else + echo -e "\e[31mCannot find Docker Compose.\e[0m" + echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m" + exit 1 + fi + +elif [ "${DOCKER_COMPOSE_VERSION}" == "native" ]; then + COMPOSE_COMMAND="docker compose" + # Check if Native Compose works and has not been deleted + if ! $COMPOSE_COMMAND > /dev/null 2>&1; then + # IF it not exists/work anymore try the other command + COMPOSE_COMMAND="docker-compose" + if ! $COMPOSE_COMMAND > /dev/null 2>&1 || ! $COMPOSE_COMMAND --version | grep "^2." > /dev/null 2>&1; then + # IF it cannot find Standalone in > 2.X, then script stops + echo -e "\e[31mCannot find Docker Compose or the Version is lower then 2.X.X.\e[0m" + echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m" + exit 1 + fi + # If it finds the standalone Plugin it will use this instead and change the mailcow.conf Variable accordingly + echo -e "\e[31mFound different Docker Compose Version then declared in mailcow.conf!\e[0m" + echo -e "\e[31mSetting the DOCKER_COMPOSE_VERSION Variable from native to standalone\e[0m" + sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=standalone/' "$SCRIPT_DIR/mailcow.conf" + sleep 2 + fi + + +elif [ "${DOCKER_COMPOSE_VERSION}" == "standalone" ]; then + COMPOSE_COMMAND="docker-compose" + # Check if Standalone Compose works and has not been deleted + if ! $COMPOSE_COMMAND > /dev/null 2>&1 && ! $COMPOSE_COMMAND --version > /dev/null 2>&1 | grep "^2." > /dev/null 2>&1; then + # IF it not exists/work anymore try the other command + COMPOSE_COMMAND="docker compose" + if ! $COMPOSE_COMMAND > /dev/null 2>&1; then + # IF it cannot find Native in > 2.X, then script stops + echo -e "\e[31mCannot find Docker Compose.\e[0m" + echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m" + exit 1 + fi + # If it finds the native Plugin it will use this instead and change the mailcow.conf Variable accordingly + echo -e "\e[31mFound different Docker Compose Version then declared in mailcow.conf!\e[0m" + echo -e "\e[31mSetting the DOCKER_COMPOSE_VERSION Variable from standalone to native\e[0m" + sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=native/' "$SCRIPT_DIR/mailcow.conf" + sleep 2 + fi +fi +} + +detect_bad_asn() { + echo -e "\e[33mDetecting if your IP is listed on Spamhaus Bad ASN List...\e[0m" + response=$(curl --connect-timeout 15 --max-time 30 -s -o /dev/null -w "%{http_code}" "https://asn-check.mailcow.email") + if [ "$response" -eq 503 ]; then + if [ -z "$SPAMHAUS_DQS_KEY" ]; then + echo -e "\e[33mYour server's public IP uses an AS that is blocked by Spamhaus to use their DNS public blocklists for Postfix.\e[0m" + echo -e "\e[33mmailcow did not detected a value for the variable SPAMHAUS_DQS_KEY inside mailcow.conf!\e[0m" + sleep 2 + echo "" + echo -e "\e[33mTo use the Spamhaus DNS Blocklists again, you will need to create a FREE account for their Data Query Service (DQS) at: https://www.spamhaus.com/free-trial/sign-up-for-a-free-data-query-service-account\e[0m" + echo -e "\e[33mOnce done, enter your DQS API key in mailcow.conf and mailcow will do the rest for you!\e[0m" + echo "" + sleep 2 + + else + echo -e "\e[33mYour server's public IP uses an AS that is blocked by Spamhaus to use their DNS public blocklists for Postfix.\e[0m" + echo -e "\e[32mmailcow detected a Value for the variable SPAMHAUS_DQS_KEY inside mailcow.conf. Postfix will use DQS with the given API key...\e[0m" + fi + elif [ "$response" -eq 200 ]; then + echo -e "\e[33mCheck completed! Your IP is \e[32mclean\e[0m" + elif [ "$response" -eq 429 ]; then + echo -e "\e[33mCheck completed! \e[31mYour IP seems to be rate limited on the ASN Check service... please try again later!\e[0m" + else + echo -e "\e[31mCheck failed! \e[0mMaybe a DNS or Network problem?\e[0m" + fi +} + +fix_broken_dnslist_conf() { + +# Fixing issue: #6143. To be removed in a later patch + + local file="${SCRIPT_DIR}/data/conf/postfix/dns_blocklists.cf" + # Check if the file exists + if [[ ! -f "$file" ]]; then + return 1 + fi + + # Check if the file contains the autogenerated comment + if grep -q "# Autogenerated by mailcow" "$file"; then + # Ask the user if custom changes were made + echo -e "\e[91mWARNING!!! \e[31mAn old version of dns_blocklists.cf has been detected which may cause a broken postfix upon startup (see: https://github.com/mailcow/mailcow-dockerized/issues/6143)...\e[0m" + echo -e "\e[31mIf you have any custom settings in there you might copy it away and adapt the changes after the file is regenerated...\e[0m" + read -p "Do you want to delete the file now and let mailcow regenerate it properly? [y/n]" response + if [[ "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + rm "$file" + echo -e "\e[32mdns_blocklists.cf has been deleted and will be properly regenerated" + return 0 + else + echo -e "\e[35mOk, not deleting it! Please make sure you take a look at postfix upon start then..." + return 2 + fi + fi + +} + +adapt_new_options() { + + CONFIG_ARRAY=( + "SKIP_LETS_ENCRYPT" + "SKIP_SOGO" + "USE_WATCHDOG" + "WATCHDOG_NOTIFY_EMAIL" + "WATCHDOG_NOTIFY_WEBHOOK" + "WATCHDOG_NOTIFY_WEBHOOK_BODY" + "WATCHDOG_NOTIFY_BAN" + "WATCHDOG_NOTIFY_START" + "WATCHDOG_EXTERNAL_CHECKS" + "WATCHDOG_SUBJECT" + "SKIP_CLAMD" + "SKIP_IP_CHECK" + "ADDITIONAL_SAN" + "DOVEADM_PORT" + "IPV4_NETWORK" + "IPV6_NETWORK" + "LOG_LINES" + "SNAT_TO_SOURCE" + "SNAT6_TO_SOURCE" + "COMPOSE_PROJECT_NAME" + "DOCKER_COMPOSE_VERSION" + "SQL_PORT" + "API_KEY" + "API_KEY_READ_ONLY" + "API_ALLOW_FROM" + "MAILDIR_GC_TIME" + "MAILDIR_SUB" + "ACL_ANYONE" + "FTS_HEAP" + "FTS_PROCS" + "SKIP_FTS" + "ENABLE_SSL_SNI" + "ALLOW_ADMIN_EMAIL_LOGIN" + "SKIP_HTTP_VERIFICATION" + "SOGO_EXPIRE_SESSION" + "REDIS_PORT" + "DOVECOT_MASTER_USER" + "DOVECOT_MASTER_PASS" + "MAILCOW_PASS_SCHEME" + "ADDITIONAL_SERVER_NAMES" + "ACME_CONTACT" + "WATCHDOG_VERBOSE" + "WEBAUTHN_ONLY_TRUSTED_VENDORS" + "SPAMHAUS_DQS_KEY" + "SKIP_UNBOUND_HEALTHCHECK" + "DISABLE_NETFILTER_ISOLATION_RULE" + "HTTP_REDIRECT" + ) + + sed -i --follow-symlinks '$a\' mailcow.conf + for option in ${CONFIG_ARRAY[@]}; do + if [[ ${option} == "ADDITIONAL_SAN" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo "${option}=" >> mailcow.conf + fi + elif [[ ${option} == "COMPOSE_PROJECT_NAME" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo "COMPOSE_PROJECT_NAME=mailcowdockerized" >> mailcow.conf + fi + elif [[ ${option} == "DOCKER_COMPOSE_VERSION" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo "# Used Docker Compose version" >> mailcow.conf + echo "# Switch here between native (compose plugin) and standalone" >> mailcow.conf + echo "# For more informations take a look at the mailcow docs regarding the configuration options." >> mailcow.conf + echo "# Normally this should be untouched but if you decided to use either of those you can switch it manually here." >> mailcow.conf + echo "# Please be aware that at least one of those variants should be installed on your maschine or mailcow will fail." >> mailcow.conf + echo "" >> mailcow.conf + echo "DOCKER_COMPOSE_VERSION=${DOCKER_COMPOSE_VERSION}" >> mailcow.conf + fi + elif [[ ${option} == "DOVEADM_PORT" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo "DOVEADM_PORT=127.0.0.1:19991" >> mailcow.conf + fi + elif [[ ${option} == "WATCHDOG_NOTIFY_EMAIL" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo "WATCHDOG_NOTIFY_EMAIL=" >> mailcow.conf + fi + elif [[ ${option} == "LOG_LINES" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Max log lines per service to keep in Redis logs' >> mailcow.conf + echo "LOG_LINES=9999" >> mailcow.conf + fi + elif [[ ${option} == "IPV4_NETWORK" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Internal IPv4 /24 subnet, format n.n.n. (expands to n.n.n.0/24)' >> mailcow.conf + echo "IPV4_NETWORK=172.22.1" >> mailcow.conf + fi + elif [[ ${option} == "IPV6_NETWORK" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Internal IPv6 subnet in fc00::/7' >> mailcow.conf + echo "IPV6_NETWORK=fd4d:6169:6c63:6f77::/64" >> mailcow.conf + fi + elif [[ ${option} == "SQL_PORT" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Bind SQL to 127.0.0.1 on port 13306' >> mailcow.conf + echo "SQL_PORT=127.0.0.1:13306" >> mailcow.conf + fi + elif [[ ${option} == "API_KEY" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Create or override API key for web UI' >> mailcow.conf + echo "#API_KEY=" >> mailcow.conf + fi + elif [[ ${option} == "API_KEY_READ_ONLY" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Create or override read-only API key for web UI' >> mailcow.conf + echo "#API_KEY_READ_ONLY=" >> mailcow.conf + fi + elif [[ ${option} == "API_ALLOW_FROM" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Must be set for API_KEY to be active' >> mailcow.conf + echo '# IPs only, no networks (networks can be set via UI)' >> mailcow.conf + echo "#API_ALLOW_FROM=" >> mailcow.conf + fi + elif [[ ${option} == "SNAT_TO_SOURCE" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Use this IPv4 for outgoing connections (SNAT)' >> mailcow.conf + echo "#SNAT_TO_SOURCE=" >> mailcow.conf + fi + elif [[ ${option} == "SNAT6_TO_SOURCE" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Use this IPv6 for outgoing connections (SNAT)' >> mailcow.conf + echo "#SNAT6_TO_SOURCE=" >> mailcow.conf + fi + elif [[ ${option} == "MAILDIR_GC_TIME" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Garbage collector cleanup' >> mailcow.conf + echo '# Deleted domains and mailboxes are moved to /var/vmail/_garbage/timestamp_sanitizedstring' >> mailcow.conf + echo '# How long should objects remain in the garbage until they are being deleted? (value in minutes)' >> mailcow.conf + echo '# Check interval is hourly' >> mailcow.conf + echo 'MAILDIR_GC_TIME=1440' >> mailcow.conf + fi + elif [[ ${option} == "ACL_ANYONE" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Set this to "allow" to enable the anyone pseudo user. Disabled by default.' >> mailcow.conf + echo '# When enabled, ACL can be created, that apply to "All authenticated users"' >> mailcow.conf + echo '# This should probably only be activated on mail hosts, that are used exclusivly by one organisation.' >> mailcow.conf + echo '# Otherwise a user might share data with too many other users.' >> mailcow.conf + echo 'ACL_ANYONE=disallow' >> mailcow.conf + fi + elif [[ ${option} == "FTS_HEAP" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Dovecot Indexing (FTS) Process maximum heap size in MB, there is no recommendation, please see Dovecot docs.' >> mailcow.conf + echo '# Flatcurve is used as FTS Engine. It is supposed to be pretty efficient in CPU and RAM consumption.' >> mailcow.conf + echo '# Please always monitor your Resource consumption!' >> mailcow.conf + echo "FTS_HEAP=128" >> mailcow.conf + fi + elif [[ ${option} == "SKIP_FTS" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Skip FTS (Fulltext Search) for Dovecot on low-memory, low-threaded systems or if you simply want to disable it.' >> mailcow.conf + echo "# Dovecot inside mailcow use Flatcurve as FTS Backend." >> mailcow.conf + echo "SKIP_FTS=y" >> mailcow.conf + fi + elif [[ ${option} == "FTS_PROCS" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Controls how many processes the Dovecot indexing process can spawn at max.' >> mailcow.conf + echo '# Too many indexing processes can use a lot of CPU and Disk I/O' >> mailcow.conf + echo '# Please visit: https://doc.dovecot.org/configuration_manual/service_configuration/#indexer-worker for more informations' >> mailcow.conf + echo "FTS_PROCS=1" >> mailcow.conf + fi + elif [[ ${option} == "ENABLE_SSL_SNI" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Create seperate certificates for all domains - y/n' >> mailcow.conf + echo '# this will allow adding more than 100 domains, but some email clients will not be able to connect with alternative hostnames' >> mailcow.conf + echo '# see https://wiki.dovecot.org/SSL/SNIClientSupport' >> mailcow.conf + echo "ENABLE_SSL_SNI=n" >> mailcow.conf + fi + elif [[ ${option} == "SKIP_SOGO" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Skip SOGo: Will disable SOGo integration and therefore webmail, DAV protocols and ActiveSync support (experimental, unsupported, not fully implemented) - y/n' >> mailcow.conf + echo "SKIP_SOGO=n" >> mailcow.conf + fi + elif [[ ${option} == "MAILDIR_SUB" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# MAILDIR_SUB defines a path in a users virtual home to keep the maildir in. Leave empty for updated setups.' >> mailcow.conf + echo "#MAILDIR_SUB=Maildir" >> mailcow.conf + echo "MAILDIR_SUB=" >> mailcow.conf + fi + elif [[ ${option} == "WATCHDOG_NOTIFY_WEBHOOK" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Send notifications to a webhook URL that receives a POST request with the content type "application/json".' >> mailcow.conf + echo '# You can use this to send notifications to services like Discord, Slack and others.' >> mailcow.conf + echo '#WATCHDOG_NOTIFY_WEBHOOK=https://discord.com/api/webhooks/XXXXXXXXXXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' >> mailcow.conf + fi + elif [[ ${option} == "WATCHDOG_NOTIFY_WEBHOOK_BODY" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# JSON body included in the webhook POST request. Needs to be in single quotes.' >> mailcow.conf + echo '# Following variables are available: SUBJECT, BODY' >> mailcow.conf + WEBHOOK_BODY='{"username": "mailcow Watchdog", "content": "**${SUBJECT}**\n${BODY}"}' + echo "#WATCHDOG_NOTIFY_WEBHOOK_BODY='${WEBHOOK_BODY}'" >> mailcow.conf + fi + elif [[ ${option} == "WATCHDOG_NOTIFY_BAN" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Notify about banned IP. Includes whois lookup.' >> mailcow.conf + echo "WATCHDOG_NOTIFY_BAN=y" >> mailcow.conf + fi + elif [[ ${option} == "WATCHDOG_NOTIFY_START" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Send a notification when the watchdog is started.' >> mailcow.conf + echo "WATCHDOG_NOTIFY_START=y" >> mailcow.conf + fi + elif [[ ${option} == "WATCHDOG_SUBJECT" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Subject for watchdog mails. Defaults to "Watchdog ALERT" followed by the error message.' >> mailcow.conf + echo "#WATCHDOG_SUBJECT=" >> mailcow.conf + fi + elif [[ ${option} == "WATCHDOG_EXTERNAL_CHECKS" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Checks if mailcow is an open relay. Requires a SAL. More checks will follow.' >> mailcow.conf + echo '# No data is collected. Opt-in and anonymous.' >> mailcow.conf + echo '# Will only work with unmodified mailcow setups.' >> mailcow.conf + echo "WATCHDOG_EXTERNAL_CHECKS=n" >> mailcow.conf + fi + elif [[ ${option} == "SOGO_EXPIRE_SESSION" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# SOGo session timeout in minutes' >> mailcow.conf + echo "SOGO_EXPIRE_SESSION=480" >> mailcow.conf + fi + elif [[ ${option} == "REDIS_PORT" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo "REDIS_PORT=127.0.0.1:7654" >> mailcow.conf + fi + elif [[ ${option} == "DOVECOT_MASTER_USER" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# DOVECOT_MASTER_USER and _PASS must _both_ be provided. No special chars.' >> mailcow.conf + echo '# Empty by default to auto-generate master user and password on start.' >> mailcow.conf + echo '# User expands to DOVECOT_MASTER_USER@mailcow.local' >> mailcow.conf + echo '# LEAVE EMPTY IF UNSURE' >> mailcow.conf + echo "DOVECOT_MASTER_USER=" >> mailcow.conf + fi + elif [[ ${option} == "DOVECOT_MASTER_PASS" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# LEAVE EMPTY IF UNSURE' >> mailcow.conf + echo "DOVECOT_MASTER_PASS=" >> mailcow.conf + fi + elif [[ ${option} == "MAILCOW_PASS_SCHEME" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Password hash algorithm' >> mailcow.conf + echo '# Only certain password hash algorithm are supported. For a fully list of supported schemes,' >> mailcow.conf + echo '# see https://docs.mailcow.email/models/model-passwd/' >> mailcow.conf + echo "MAILCOW_PASS_SCHEME=BLF-CRYPT" >> mailcow.conf + fi + elif [[ ${option} == "ADDITIONAL_SERVER_NAMES" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Additional server names for mailcow UI' >> mailcow.conf + echo '#' >> mailcow.conf + echo '# Specify alternative addresses for the mailcow UI to respond to' >> mailcow.conf + echo '# This is useful when you set mail.* as ADDITIONAL_SAN and want to make sure mail.maildomain.com will always point to the mailcow UI.' >> mailcow.conf + echo '# If the server name does not match a known site, Nginx decides by best-guess and may redirect users to the wrong web root.' >> mailcow.conf + echo '# You can understand this as server_name directive in Nginx.' >> mailcow.conf + echo '# Comma separated list without spaces! Example: ADDITIONAL_SERVER_NAMES=a.b.c,d.e.f' >> mailcow.conf + echo 'ADDITIONAL_SERVER_NAMES=' >> mailcow.conf + fi + elif [[ ${option} == "ACME_CONTACT" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Lets Encrypt registration contact information' >> mailcow.conf + echo '# Optional: Leave empty for none' >> mailcow.conf + echo '# This value is only used on first order!' >> mailcow.conf + echo '# Setting it at a later point will require the following steps:' >> mailcow.conf + echo '# https://docs.mailcow.email/troubleshooting/debug-reset_tls/' >> mailcow.conf + echo 'ACME_CONTACT=' >> mailcow.conf + fi + elif [[ ${option} == "WEBAUTHN_ONLY_TRUSTED_VENDORS" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo "# WebAuthn device manufacturer verification" >> mailcow.conf + echo '# After setting WEBAUTHN_ONLY_TRUSTED_VENDORS=y only devices from trusted manufacturers are allowed' >> mailcow.conf + echo '# root certificates can be placed for validation under mailcow-dockerized/data/web/inc/lib/WebAuthn/rootCertificates' >> mailcow.conf + echo 'WEBAUTHN_ONLY_TRUSTED_VENDORS=n' >> mailcow.conf + fi + elif [[ ${option} == "SPAMHAUS_DQS_KEY" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo "# Spamhaus Data Query Service Key" >> mailcow.conf + echo '# Optional: Leave empty for none' >> mailcow.conf + echo '# Enter your key here if you are using a blocked ASN (OVH, AWS, Cloudflare e.g) for the unregistered Spamhaus Blocklist.' >> mailcow.conf + echo '# If empty, it will completely disable Spamhaus blocklists if it detects that you are running on a server using a blocked AS.' >> mailcow.conf + echo '# Otherwise it will work as usual.' >> mailcow.conf + echo 'SPAMHAUS_DQS_KEY=' >> mailcow.conf + fi + elif [[ ${option} == "WATCHDOG_VERBOSE" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Enable watchdog verbose logging' >> mailcow.conf + echo 'WATCHDOG_VERBOSE=n' >> mailcow.conf + fi + elif [[ ${option} == "SKIP_UNBOUND_HEALTHCHECK" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Skip Unbound (DNS Resolver) Healthchecks (NOT Recommended!) - y/n' >> mailcow.conf + echo 'SKIP_UNBOUND_HEALTHCHECK=n' >> mailcow.conf + fi + elif [[ ${option} == "DISABLE_NETFILTER_ISOLATION_RULE" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Prevent netfilter from setting an iptables/nftables rule to isolate the mailcow docker network - y/n' >> mailcow.conf + echo '# CAUTION: Disabling this may expose container ports to other neighbors on the same subnet, even if the ports are bound to localhost' >> mailcow.conf + echo 'DISABLE_NETFILTER_ISOLATION_RULE=n' >> mailcow.conf + fi + elif [[ ${option} == "HTTP_REDIRECT" ]]; then + if ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Redirect HTTP connections to HTTPS - y/n' >> mailcow.conf + echo 'HTTP_REDIRECT=n' >> mailcow.conf + fi + elif ! grep -q ${option} mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo "${option}=n" >> mailcow.conf + fi + done +} + +migrate_solr_config_options() { + + sed -i --follow-symlinks '$a\' mailcow.conf + + if grep -q "SOLR_HEAP" mailcow.conf; then + echo "Removing SOLR_HEAP in mailcow.conf" + sed -i '/# Solr heap size in MB\b/d' mailcow.conf + sed -i '/# Solr is a prone to run\b/d' mailcow.conf + sed -i '/SOLR_HEAP\b/d' mailcow.conf + fi + + if grep -q "SKIP_SOLR" mailcow.conf; then + echo "Removing SKIP_SOLR in mailcow.conf" + sed -i '/\bSkip Solr on low-memory\b/d' mailcow.conf + sed -i '/\bSolr is disabled by default\b/d' mailcow.conf + sed -i '/\bDisable Solr or\b/d' mailcow.conf + sed -i '/\bSKIP_SOLR\b/d' mailcow.conf + fi + + if grep -q "SOLR_PORT" mailcow.conf; then + echo "Removing SOLR_PORT in mailcow.conf" + sed -i '/\bSOLR_PORT\b/d' mailcow.conf + fi + + if grep -q "FLATCURVE_EXPERIMENTAL" mailcow.conf; then + echo "Removing FLATCURVE_EXPERIMENTAL in mailcow.conf" + sed -i '/\bFLATCURVE_EXPERIMENTAL\b/d' mailcow.conf + fi + + solr_volume=$(docker volume ls -qf name=^${COMPOSE_PROJECT_NAME}_solr-vol-1) + if [[ -n $solr_volume ]]; then + echo -e "\e[34mSolr has been replaced within mailcow since 2025-01.\nThe volume $solr_volume is unused.\e[0m" + sleep 1 + if [ ! "$FORCE" ]; then + read -r -p "Remove $solr_volume? [y/N] " response + if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + echo -e "\e[33mRemoving $solr_volume...\e[0m" + docker volume rm $solr_volume || echo -e "\e[31mFailed to remove. Remove it manually!\e[0m" + echo -e "\e[32mSuccessfully removed $solr_volume!\e[0m" + else + echo -e "Not removing $solr_volume. Run \`docker volume rm $solr_volume\` manually if needed." + fi + else + echo -e "\e[33mForce removing $solr_volume...\e[0m" + docker volume rm $solr_volume || echo -e "\e[31mFailed to remove. Remove it manually!\e[0m" + echo -e "\e[32mSuccessfully removed $solr_volume!\e[0m" + fi + fi + + # Delete old fts.conf before forced switch to flatcurve to ensure update is working properly + FTS_CONF_PATH="${SCRIPT_DIR}/data/conf/dovecot/conf.d/fts.conf" + if [[ -f "$FTS_CONF_PATH" ]]; then + if grep -q "Autogenerated by mailcow" "$FTS_CONF_PATH"; then + rm -rf $FTS_CONF_PATH + fi + fi +} + +detect_major_update() { + if [ ${BRANCH} == "master" ]; then + # Array with major versions + # Add major versions here + MAJOR_VERSIONS=( + "2025-02" + "2025-03" + ) + + current_version="" + if [[ -f "${SCRIPT_DIR}/data/web/inc/app_info.inc.php" ]]; then + current_version=$(grep 'MAILCOW_GIT_VERSION' ${SCRIPT_DIR}/data/web/inc/app_info.inc.php | sed -E 's/.*MAILCOW_GIT_VERSION="([^"]+)".*/\1/') + fi + if [[ -z "$current_version" ]]; then + return 1 + fi + release_url="https://github.com/mailcow/mailcow-dockerized/releases/tag" + + updates_to_apply=() + + for version in "${MAJOR_VERSIONS[@]}"; do + if [[ "$current_version" < "$version" ]]; then + updates_to_apply+=("$version") + fi + done + + if [[ ${#updates_to_apply[@]} -gt 0 ]]; then + echo -e "\e[33m\nMAJOR UPDATES to be applied:\e[0m" + for update in "${updates_to_apply[@]}"; do + echo "$update - $release_url/$update" + done + + echo -e "\nPlease read the release notes before proceeding." + read -p "Do you want to proceed with the update? [y/n] " response + if [[ "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + echo "Proceeding with the update..." + else + echo "Update canceled. Exiting." + exit 1 + fi + fi + fi +} + +############## End Function Section ############## + +# Check permissions +if [ "$(id -u)" -ne "0" ]; then + echo "You need to be root" + exit 1 +fi + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +# Run pre-update-hook +if [ -f "${SCRIPT_DIR}/pre_update_hook.sh" ]; then + bash "${SCRIPT_DIR}/pre_update_hook.sh" +fi + +if [[ "$(uname -r)" =~ ^4\.15\.0-60 ]]; then + echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!"; + echo "Please update to 5.x or use another distribution." + exit 1 +fi + +if [[ "$(uname -r)" =~ ^4\.4\. ]]; then + if grep -q Ubuntu <<< "$(uname -a)"; then + echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!" + echo "Please update to linux-generic-hwe-16.04 by running \"apt-get install --install-recommends linux-generic-hwe-16.04\"" + exit 1 + fi + echo "mailcow on a 4.4.x kernel is not supported. It may or may not work, please upgrade your kernel or continue at your own risk." + read -p "Press any key to continue..." < /dev/tty +fi + +# Exit on error and pipefail +set -o pipefail + +# Setting high dc timeout +export COMPOSE_HTTP_TIMEOUT=600 + +# Add /opt/bin to PATH +PATH=$PATH:/opt/bin + +umask 0022 + +# Unset COMPOSE_COMMAND and DOCKER_COMPOSE_VERSION Variable to be on the newest state. +unset COMPOSE_COMMAND +unset DOCKER_COMPOSE_VERSION + +for bin in curl docker git awk sha1sum grep cut; do + if [[ -z $(command -v ${bin}) ]]; then + echo "Cannot find ${bin}, exiting..." + exit 1; + fi +done + +# Check Docker Version (need at least 24.X) +docker_version=$(docker -v | grep -oP '\d+\.\d+\.\d+' | cut -d '.' -f 1 | head -1) + +if [[ $docker_version -lt 24 ]]; then + echo -e "\e[31mCannot find Docker with a Version higher or equals 24.0.0\e[0m" + echo -e "\e[33mmailcow needs a newer Docker version to work properly... continuing on your own risk!\e[0m" + echo -e "\e[31mPlease update your Docker installation... sleeping 10s\e[0m" + sleep 10 +fi + +export LC_ALL=C +DATE=$(date +%Y-%m-%d_%H_%M_%S) +BRANCH="$(cd "${SCRIPT_DIR}"; git rev-parse --abbrev-ref HEAD)" + +while (($#)); do + case "${1}" in + --check|-c) + echo "Checking remote code for updates..." + LATEST_REV=$(git ls-remote --exit-code --refs --quiet https://github.com/mailcow/mailcow-dockerized "${BRANCH}" | cut -f1) + if [ "$?" -ne 0 ]; then + echo "A problem occurred while trying to fetch the latest revision from github." + exit 99 + fi + if [[ -z $(git log HEAD --pretty=format:"%H" | grep "${LATEST_REV}") ]]; then + echo -e "Updated code is available.\nThe changes can be found here: https://github.com/mailcow/mailcow-dockerized/commits/master" + git log --date=short --pretty=format:"%ad - %s" "$(git rev-parse --short HEAD)"..origin/master + exit 0 + else + echo "No updates available." + exit 3 + fi + ;; + --check-tags) + echo "Checking remote tags for updates..." + LATEST_TAG_REV=$(git ls-remote --exit-code --quiet --tags origin | tail -1 | cut -f1) + if [ "$?" -ne 0 ]; then + echo "A problem occurred while trying to fetch the latest tag from github." + exit 99 + fi + if [[ -z $(git log HEAD --pretty=format:"%H" | grep "${LATEST_TAG_REV}") ]]; then + echo -e "New tag is available.\nThe changes can be found here: https://github.com/mailcow/mailcow-dockerized/releases/latest" + exit 0 + else + echo "No updates available." + exit 3 + fi + ;; + --ours) + MERGE_STRATEGY=ours + ;; + --skip-start) + SKIP_START=y + ;; + --skip-ping-check) + SKIP_PING_CHECK=y + ;; + --stable) + CURRENT_BRANCH="$(cd "${SCRIPT_DIR}"; git rev-parse --abbrev-ref HEAD)" + NEW_BRANCH="master" + ;; + --gc) + echo -e "\e[32mCollecting garbage...\e[0m" + docker_garbage + exit 0 + ;; + --nightly) + CURRENT_BRANCH="$(cd "${SCRIPT_DIR}"; git rev-parse --abbrev-ref HEAD)" + NEW_BRANCH="nightly" + ;; + --prefetch) + echo -e "\e[32mPrefetching images...\e[0m" + prefetch_images + exit 0 + ;; + -f|--force) + echo -e "\e[32mRunning in forced mode...\e[0m" + FORCE=y + ;; + -d|--dev) + echo -e "\e[32mRunning in Developer mode...\e[0m" + DEV=y + ;; + --legacy) + CURRENT_BRANCH="$(cd "${SCRIPT_DIR}"; git rev-parse --abbrev-ref HEAD)" + NEW_BRANCH="legacy" + ;; + --help|-h) + echo './update.sh [-c|--check, --check-tags, --ours, --gc, --nightly, --prefetch, --skip-start, --skip-ping-check, --stable, --legacy, -f|--force, -d|--dev, -h|--help] + + -c|--check - Check for updates and exit (exit codes => 0: update available, 3: no updates) + --check-tags - Check for newer tags and exit (exit codes => 0: newer tag available, 3: no newer tag) + --ours - Use merge strategy option "ours" to solve conflicts in favor of non-mailcow code (local changes over remote changes), not recommended! + --gc - Run garbage collector to delete old image tags + --nightly - Switch your mailcow updates to the unstable (nightly) branch. FOR TESTING PURPOSES ONLY!!!! + --prefetch - Only prefetch new images and exit (useful to prepare updates) + --skip-start - Do not start mailcow after update + --skip-ping-check - Skip ICMP Check to public DNS resolvers (Use it only if you'\''ve blocked any ICMP Connections to your mailcow machine) + --stable - Switch your mailcow updates to the stable (master) branch. Default unless you changed it with --nightly or --legacy. + --legacy - Switch your mailcow updates to the legacy branch. The legacy branch will only receive security updates until February 2026. + -f|--force - Force update, do not ask questions + -d|--dev - Enables Developer Mode (No Checkout of update.sh for tests) +' + exit 0 + esac + shift +done + +[[ ! -f mailcow.conf ]] && { echo -e "\e[31mmailcow.conf is missing! Is mailcow installed?\e[0m"; exit 1;} + +chmod 600 mailcow.conf +source mailcow.conf + +detect_docker_compose_command + +fix_broken_dnslist_conf + +DOTS=${MAILCOW_HOSTNAME//[^.]}; +if [ ${#DOTS} -lt 1 ]; then + echo -e "\e[31mMAILCOW_HOSTNAME (${MAILCOW_HOSTNAME}) is not a FQDN!\e[0m" + sleep 1 + echo "Please change it to a FQDN and redeploy the stack with $COMPOSE_COMMAND up -d" + exit 1 +elif [[ "${MAILCOW_HOSTNAME: -1}" == "." ]]; then + echo "MAILCOW_HOSTNAME (${MAILCOW_HOSTNAME}) is ending with a dot. This is not a valid FQDN!" + exit 1 +elif [ ${#DOTS} -eq 1 ]; then + echo -e "\e[33mMAILCOW_HOSTNAME (${MAILCOW_HOSTNAME}) does not contain a Subdomain. This is not fully tested and may cause issues.\e[0m" + echo "Find more information about why this message exists here: https://github.com/mailcow/mailcow-dockerized/issues/1572" + read -r -p "Do you want to proceed anyway? [y/N] " response + if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + echo "OK. Proceeding." + else + echo "OK. Exiting." + exit 1 + fi +fi + +if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo "BusyBox grep detected, please install gnu grep, \"apk add --no-cache --upgrade grep\""; exit 1; fi +# This will also cover sort +if cp --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo "BusyBox cp detected, please install coreutils, \"apk add --no-cache --upgrade coreutils\""; exit 1; fi +if sed --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo "BusyBox sed detected, please install gnu sed, \"apk add --no-cache --upgrade sed\""; exit 1; fi + +CONFIG_ARRAY=( + "SKIP_LETS_ENCRYPT" + "SKIP_SOGO" + "USE_WATCHDOG" + "WATCHDOG_NOTIFY_EMAIL" + "WATCHDOG_NOTIFY_WEBHOOK" + "WATCHDOG_NOTIFY_WEBHOOK_BODY" + "WATCHDOG_NOTIFY_BAN" + "WATCHDOG_NOTIFY_START" + "WATCHDOG_EXTERNAL_CHECKS" + "WATCHDOG_SUBJECT" + "SKIP_CLAMD" + "SKIP_IP_CHECK" + "ADDITIONAL_SAN" + "AUTODISCOVER_SAN" + "DOVEADM_PORT" + "IPV4_NETWORK" + "IPV6_NETWORK" + "LOG_LINES" + "SNAT_TO_SOURCE" + "SNAT6_TO_SOURCE" + "COMPOSE_PROJECT_NAME" + "DOCKER_COMPOSE_VERSION" + "SQL_PORT" + "API_KEY" + "API_KEY_READ_ONLY" + "API_ALLOW_FROM" + "MAILDIR_GC_TIME" + "MAILDIR_SUB" + "ACL_ANYONE" + "ENABLE_SSL_SNI" + "ALLOW_ADMIN_EMAIL_LOGIN" + "SKIP_HTTP_VERIFICATION" + "SOGO_EXPIRE_SESSION" + "REDIS_PORT" + "DOVECOT_MASTER_USER" + "DOVECOT_MASTER_PASS" + "MAILCOW_PASS_SCHEME" + "ADDITIONAL_SERVER_NAMES" + "ACME_CONTACT" + "WATCHDOG_VERBOSE" + "WEBAUTHN_ONLY_TRUSTED_VENDORS" + "SPAMHAUS_DQS_KEY" + "SKIP_UNBOUND_HEALTHCHECK" + "DISABLE_NETFILTER_ISOLATION_RULE" + "REDISPASS" +) + +detect_bad_asn + +sed -i --follow-symlinks '$a\' mailcow.conf +for option in "${CONFIG_ARRAY[@]}"; do + if [[ ${option} == "ADDITIONAL_SAN" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo "${option}=" >> mailcow.conf + fi + elif [[ "${option}" == "COMPOSE_PROJECT_NAME" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo "COMPOSE_PROJECT_NAME=mailcowdockerized" >> mailcow.conf + fi + elif [[ "${option}" == "DOCKER_COMPOSE_VERSION" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo "# Used Docker Compose version" >> mailcow.conf + echo "# Switch here between native (compose plugin) and standalone" >> mailcow.conf + echo "# For more informations take a look at the mailcow docs regarding the configuration options." >> mailcow.conf + echo "# Normally this should be untouched but if you decided to use either of those you can switch it manually here." >> mailcow.conf + echo "# Please be aware that at least one of those variants should be installed on your maschine or mailcow will fail." >> mailcow.conf + echo "" >> mailcow.conf + echo "DOCKER_COMPOSE_VERSION=${DOCKER_COMPOSE_VERSION}" >> mailcow.conf + fi + elif [[ "${option}" == "DOVEADM_PORT" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo "DOVEADM_PORT=127.0.0.1:19991" >> mailcow.conf + fi + elif [[ "${option}" == "WATCHDOG_NOTIFY_EMAIL" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo "WATCHDOG_NOTIFY_EMAIL=" >> mailcow.conf + fi + elif [[ "${option}" == "LOG_LINES" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Max log lines per service to keep in Redis logs' >> mailcow.conf + echo "LOG_LINES=9999" >> mailcow.conf + fi + elif [[ "${option}" == "IPV4_NETWORK" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Internal IPv4 /24 subnet, format n.n.n. (expands to n.n.n.0/24)' >> mailcow.conf + echo "IPV4_NETWORK=172.22.1" >> mailcow.conf + fi + elif [[ "${option}" == "IPV6_NETWORK" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Internal IPv6 subnet in fc00::/7' >> mailcow.conf + echo "IPV6_NETWORK=fd4d:6169:6c63:6f77::/64" >> mailcow.conf + fi + elif [[ "${option}" == "SQL_PORT" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Bind SQL to 127.0.0.1 on port 13306' >> mailcow.conf + echo "SQL_PORT=127.0.0.1:13306" >> mailcow.conf + fi + elif [[ "${option}" == "API_KEY" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Create or override API key for web UI' >> mailcow.conf + echo "#API_KEY=" >> mailcow.conf + fi + elif [[ "${option}" == "API_KEY_READ_ONLY" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Create or override read-only API key for web UI' >> mailcow.conf + echo "#API_KEY_READ_ONLY=" >> mailcow.conf + fi + elif [[ "${option}" == "API_ALLOW_FROM" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Must be set for API_KEY to be active' >> mailcow.conf + echo '# IPs only, no networks (networks can be set via UI)' >> mailcow.conf + echo "#API_ALLOW_FROM=" >> mailcow.conf + fi + elif [[ "${option}" == "SNAT_TO_SOURCE" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Use this IPv4 for outgoing connections (SNAT)' >> mailcow.conf + echo "#SNAT_TO_SOURCE=" >> mailcow.conf + fi + elif [[ "${option}" == "SNAT6_TO_SOURCE" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Use this IPv6 for outgoing connections (SNAT)' >> mailcow.conf + echo "#SNAT6_TO_SOURCE=" >> mailcow.conf + fi + elif [[ "${option}" == "MAILDIR_GC_TIME" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Garbage collector cleanup' >> mailcow.conf + echo '# Deleted domains and mailboxes are moved to /var/vmail/_garbage/timestamp_sanitizedstring' >> mailcow.conf + echo '# How long should objects remain in the garbage until they are being deleted? (value in minutes)' >> mailcow.conf + echo '# Check interval is hourly' >> mailcow.conf + echo 'MAILDIR_GC_TIME=1440' >> mailcow.conf + fi + elif [[ "${option}" == "ACL_ANYONE" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Set this to "allow" to enable the anyone pseudo user. Disabled by default.' >> mailcow.conf + echo '# When enabled, ACL can be created, that apply to "All authenticated users"' >> mailcow.conf + echo '# This should probably only be activated on mail hosts, that are used exclusivly by one organisation.' >> mailcow.conf + echo '# Otherwise a user might share data with too many other users.' >> mailcow.conf + echo 'ACL_ANYONE=disallow' >> mailcow.conf + fi + elif [[ "${option}" == "ENABLE_SSL_SNI" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Create seperate certificates for all domains - y/n' >> mailcow.conf + echo '# this will allow adding more than 100 domains, but some email clients will not be able to connect with alternative hostnames' >> mailcow.conf + echo '# see https://wiki.dovecot.org/SSL/SNIClientSupport' >> mailcow.conf + echo "ENABLE_SSL_SNI=n" >> mailcow.conf + fi + elif [[ "${option}" == "SKIP_SOGO" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Skip SOGo: Will disable SOGo integration and therefore webmail, DAV protocols and ActiveSync support (experimental, unsupported, not fully implemented) - y/n' >> mailcow.conf + echo "SKIP_SOGO=n" >> mailcow.conf + fi + elif [[ "${option}" == "MAILDIR_SUB" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# MAILDIR_SUB defines a path in a users virtual home to keep the maildir in. Leave empty for updated setups.' >> mailcow.conf + echo "#MAILDIR_SUB=Maildir" >> mailcow.conf + echo "MAILDIR_SUB=" >> mailcow.conf + fi + elif [[ "${option}" == "WATCHDOG_NOTIFY_WEBHOOK" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Send notifications to a webhook URL that receives a POST request with the content type "application/json".' >> mailcow.conf + echo '# You can use this to send notifications to services like Discord, Slack and others.' >> mailcow.conf + echo '#WATCHDOG_NOTIFY_WEBHOOK=https://discord.com/api/webhooks/XXXXXXXXXXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' >> mailcow.conf + fi + elif [[ "${option}" == "WATCHDOG_NOTIFY_WEBHOOK_BODY" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# JSON body included in the webhook POST request. Needs to be in single quotes.' >> mailcow.conf + echo '# Following variables are available: SUBJECT, BODY' >> mailcow.conf + WEBHOOK_BODY='{"username": "mailcow Watchdog", "content": "**${SUBJECT}**\n${BODY}"}' + echo "#WATCHDOG_NOTIFY_WEBHOOK_BODY='${WEBHOOK_BODY}'" >> mailcow.conf + fi + elif [[ "${option}" == "WATCHDOG_NOTIFY_BAN" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Notify about banned IP. Includes whois lookup.' >> mailcow.conf + echo "WATCHDOG_NOTIFY_BAN=y" >> mailcow.conf + fi + elif [[ "${option}" == "WATCHDOG_NOTIFY_START" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Send a notification when the watchdog is started.' >> mailcow.conf + echo "WATCHDOG_NOTIFY_START=y" >> mailcow.conf + fi + elif [[ "${option}" == "WATCHDOG_SUBJECT" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Subject for watchdog mails. Defaults to "Watchdog ALERT" followed by the error message.' >> mailcow.conf + echo "#WATCHDOG_SUBJECT=" >> mailcow.conf + fi + elif [[ "${option}" == "WATCHDOG_EXTERNAL_CHECKS" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Checks if mailcow is an open relay. Requires a SAL. More checks will follow.' >> mailcow.conf + echo '# No data is collected. Opt-in and anonymous.' >> mailcow.conf + echo '# Will only work with unmodified mailcow setups.' >> mailcow.conf + echo "WATCHDOG_EXTERNAL_CHECKS=n" >> mailcow.conf + fi + elif [[ "${option}" == "SOGO_EXPIRE_SESSION" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# SOGo session timeout in minutes' >> mailcow.conf + echo "SOGO_EXPIRE_SESSION=480" >> mailcow.conf + fi + elif [[ "${option}" == "REDIS_PORT" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo "REDIS_PORT=127.0.0.1:7654" >> mailcow.conf + fi + elif [[ "${option}" == "DOVECOT_MASTER_USER" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# DOVECOT_MASTER_USER and _PASS must _both_ be provided. No special chars.' >> mailcow.conf + echo '# Empty by default to auto-generate master user and password on start.' >> mailcow.conf + echo '# User expands to DOVECOT_MASTER_USER@mailcow.local' >> mailcow.conf + echo '# LEAVE EMPTY IF UNSURE' >> mailcow.conf + echo "DOVECOT_MASTER_USER=" >> mailcow.conf + fi + elif [[ "${option}" == "DOVECOT_MASTER_PASS" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# LEAVE EMPTY IF UNSURE' >> mailcow.conf + echo "DOVECOT_MASTER_PASS=" >> mailcow.conf + fi + elif [[ "${option}" == "MAILCOW_PASS_SCHEME" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Password hash algorithm' >> mailcow.conf + echo '# Only certain password hash algorithm are supported. For a fully list of supported schemes,' >> mailcow.conf + echo '# see https://docs.mailcow.email/models/model-passwd/' >> mailcow.conf + echo "MAILCOW_PASS_SCHEME=BLF-CRYPT" >> mailcow.conf + fi + elif [[ "${option}" == "ADDITIONAL_SERVER_NAMES" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Additional server names for mailcow UI' >> mailcow.conf + echo '#' >> mailcow.conf + echo '# Specify alternative addresses for the mailcow UI to respond to' >> mailcow.conf + echo '# This is useful when you set mail.* as ADDITIONAL_SAN and want to make sure mail.maildomain.com will always point to the mailcow UI.' >> mailcow.conf + echo '# If the server name does not match a known site, Nginx decides by best-guess and may redirect users to the wrong web root.' >> mailcow.conf + echo '# You can understand this as server_name directive in Nginx.' >> mailcow.conf + echo '# Comma separated list without spaces! Example: ADDITIONAL_SERVER_NAMES=a.b.c,d.e.f' >> mailcow.conf + echo 'ADDITIONAL_SERVER_NAMES=' >> mailcow.conf + fi + + elif [[ "${option}" == "AUTODISCOVER_SAN" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Obtain certificates for autodiscover.* and autoconfig.* domains.' >> mailcow.conf + echo '# This can be useful to switch off in case you are in a scenario where a reverse proxy already handles those.' >> mailcow.conf + echo '# There are mixed scenarios where ports 80,443 are occupied and you do not want to share certs' >> mailcow.conf + echo '# between services. So acme-mailcow obtains for maildomains and all web-things get handled' >> mailcow.conf + echo '# in the reverse proxy.' >> mailcow.conf + echo 'AUTODISCOVER_SAN=y' >> mailcow.conf + fi + + elif [[ "${option}" == "ACME_CONTACT" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Lets Encrypt registration contact information' >> mailcow.conf + echo '# Optional: Leave empty for none' >> mailcow.conf + echo '# This value is only used on first order!' >> mailcow.conf + echo '# Setting it at a later point will require the following steps:' >> mailcow.conf + echo '# https://docs.mailcow.email/troubleshooting/debug-reset_tls/' >> mailcow.conf + echo 'ACME_CONTACT=' >> mailcow.conf + fi + elif [[ "${option}" == "WEBAUTHN_ONLY_TRUSTED_VENDORS" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo "# WebAuthn device manufacturer verification" >> mailcow.conf + echo '# After setting WEBAUTHN_ONLY_TRUSTED_VENDORS=y only devices from trusted manufacturers are allowed' >> mailcow.conf + echo '# root certificates can be placed for validation under mailcow-dockerized/data/web/inc/lib/WebAuthn/rootCertificates' >> mailcow.conf + echo 'WEBAUTHN_ONLY_TRUSTED_VENDORS=n' >> mailcow.conf + fi + elif [[ "${option}" == "SPAMHAUS_DQS_KEY" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo "# Spamhaus Data Query Service Key" >> mailcow.conf + echo '# Optional: Leave empty for none' >> mailcow.conf + echo '# Enter your key here if you are using a blocked ASN (OVH, AWS, Cloudflare e.g) for the unregistered Spamhaus Blocklist.' >> mailcow.conf + echo '# If empty, it will completely disable Spamhaus blocklists if it detects that you are running on a server using a blocked AS.' >> mailcow.conf + echo '# Otherwise it will work as usual.' >> mailcow.conf + echo 'SPAMHAUS_DQS_KEY=' >> mailcow.conf + fi + elif [[ "${option}" == "WATCHDOG_VERBOSE" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Enable watchdog verbose logging' >> mailcow.conf + echo 'WATCHDOG_VERBOSE=n' >> mailcow.conf + fi + elif [[ "${option}" == "SKIP_UNBOUND_HEALTHCHECK" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Skip Unbound (DNS Resolver) Healthchecks (NOT Recommended!) - y/n' >> mailcow.conf + echo 'SKIP_UNBOUND_HEALTHCHECK=n' >> mailcow.conf + fi + elif [[ "${option}" == "DISABLE_NETFILTER_ISOLATION_RULE" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo '# Prevent netfilter from setting an iptables/nftables rule to isolate the mailcow docker network - y/n' >> mailcow.conf + echo '# CAUTION: Disabling this may expose container ports to other neighbors on the same subnet, even if the ports are bound to localhost' >> mailcow.conf + echo 'DISABLE_NETFILTER_ISOLATION_RULE=n' >> mailcow.conf + fi + elif [[ "${option}" == "REDISPASS" ]]; then + if ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo -e '\n# ------------------------------' >> mailcow.conf + echo '# REDIS configuration' >> mailcow.conf + echo -e '# ------------------------------\n' >> mailcow.conf + echo "REDISPASS=$(LC_ALL=C /dev/null | head -c 28)" >> mailcow.conf + fi + elif ! grep -q "${option}" mailcow.conf; then + echo "Adding new option \"${option}\" to mailcow.conf" + echo "${option}=n" >> mailcow.conf + fi +done + +if [[ ("${SKIP_PING_CHECK}" == "y") ]]; then +echo -e "\e[32mSkipping Ping Check...\e[0m" + +else + echo -en "Checking internet connection... " + if ! check_online_status; then + echo -e "\e[31mfailed\e[0m" + exit 1 + else + echo -e "\e[32mOK\e[0m" + fi +fi + +if ! [ "$NEW_BRANCH" ]; then + echo -e "\e[33mDetecting which build your mailcow runs on...\e[0m" + sleep 1 + if [ "${BRANCH}" == "master" ]; then + echo -e "\e[32mYou are receiving stable updates (master).\e[0m" + echo -e "\e[33mTo change that run the update.sh Script one time with the --nightly parameter to switch to nightly builds.\e[0m" + + elif [ "${BRANCH}" == "nightly" ]; then + echo -e "\e[31mYou are receiving unstable updates (nightly). These are for testing purposes only!!!\e[0m" + sleep 1 + echo -e "\e[33mTo change that run the update.sh Script one time with the --stable parameter to switch to stable builds.\e[0m" + + elif [ "${BRANCH}" == "legacy" ]; then + echo -e "\e[31mYou are receiving legacy updates. The legacy branch will only receive security updates until February 2026.\e[0m" + sleep 1 + echo -e "\e[33mTo change that run the update.sh Script one time with the --stable parameter to switch to stable builds.\e[0m" + + else + echo -e "\e[33mYou are receiving updates from an unsupported branch.\e[0m" + sleep 1 + echo -e "\e[33mThe mailcow stack might still work but it is recommended to switch to the master branch (stable builds).\e[0m" + echo -e "\e[33mTo change that run the update.sh Script one time with the --stable parameter to switch to stable builds.\e[0m" + fi +elif [ "$FORCE" ]; then + echo -e "\e[31mYou are running in forced mode!\e[0m" + echo -e "\e[31mA Branch Switch can only be performed manually (monitored).\e[0m" + echo -e "\e[31mPlease rerun the update.sh Script without the --force/-f parameter.\e[0m" + sleep 1 +elif [ "$NEW_BRANCH" == "master" ] && [ "$CURRENT_BRANCH" != "master" ]; then + echo -e "\e[33mYou are about to switch your mailcow updates to the stable (master) branch.\e[0m" + sleep 1 + echo -e "\e[33mBefore you do: Please take a backup of all components to ensure that no data is lost...\e[0m" + sleep 1 + echo -e "\e[31mWARNING: Please see on GitHub or ask in the community if a switch to master is stable or not. + In some rear cases an update back to master can destroy your mailcow configuration such as database upgrade, etc. + Normally an upgrade back to master should be safe during each full release. + Check GitHub for Database changes and update only if there similar to the full release!\e[0m" + read -r -p "Are you sure you that want to continue upgrading to the stable (master) branch? [y/N] " response + if [[ ! "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + echo "OK. If you prepared yourself for that please run the update.sh Script with the --stable parameter again to trigger this process here." + exit 0 + fi + BRANCH="$NEW_BRANCH" + DIFF_DIRECTORY=update_diffs + DIFF_FILE="${DIFF_DIRECTORY}/diff_before_upgrade_to_master_$(date +"%Y-%m-%d-%H-%M-%S")" + mv diff_before_upgrade* "${DIFF_DIRECTORY}/" 2> /dev/null + if ! git diff-index --quiet HEAD; then + echo -e "\e[32mSaving diff to ${DIFF_FILE}...\e[0m" + mkdir -p "${DIFF_DIRECTORY}" + git diff "${BRANCH}" --stat > "${DIFF_FILE}" + git diff "${BRANCH}" >> "${DIFF_FILE}" + fi + echo -e "\e[32mSwitching Branch to ${BRANCH}...\e[0m" + git fetch origin + git checkout -f "${BRANCH}" + +elif [ "$NEW_BRANCH" == "nightly" ] && [ "$CURRENT_BRANCH" != "nightly" ]; then + echo -e "\e[33mYou are about to switch your mailcow Updates to the unstable (nightly) branch.\e[0m" + sleep 1 + echo -e "\e[33mBefore you do: Please take a backup of all components to ensure that no Data is lost...\e[0m" + sleep 1 + echo -e "\e[31mWARNING: A switch to nightly is possible any time. But a switch back (to master) isn't.\e[0m" + read -r -p "Are you sure you that want to continue upgrading to the unstable (nightly) branch? [y/N] " response + if [[ ! "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + echo "OK. If you prepared yourself for that please run the update.sh Script with the --nightly parameter again to trigger this process here." + exit 0 + fi + BRANCH=$NEW_BRANCH + DIFF_DIRECTORY=update_diffs + DIFF_FILE=${DIFF_DIRECTORY}/diff_before_upgrade_to_nightly_$(date +"%Y-%m-%d-%H-%M-%S") + mv diff_before_upgrade* ${DIFF_DIRECTORY}/ 2> /dev/null + if ! git diff-index --quiet HEAD; then + echo -e "\e[32mSaving diff to ${DIFF_FILE}...\e[0m" + mkdir -p ${DIFF_DIRECTORY} + git diff "${BRANCH}" --stat > "${DIFF_FILE}" + git diff "${BRANCH}" >> "${DIFF_FILE}" + fi + git fetch origin + git checkout -f "${BRANCH}" +elif [ "$NEW_BRANCH" == "legacy" ] && [ "$CURRENT_BRANCH" != "legacy" ]; then + echo -e "\e[33mYou are about to switch your mailcow Updates to the legacy branch.\e[0m" + sleep 1 + echo -e "\e[33mBefore you do: Please take a backup of all components to ensure that no Data is lost...\e[0m" + sleep 1 + echo -e "\e[31mWARNING: A switch to stable or nightly is possible any time.\e[0m" + read -r -p "Are you sure you want to continue upgrading to the legacy branch? [y/N] " response + if [[ ! "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + echo "OK. If you prepared yourself for that please run the update.sh Script with the --legacy parameter again to trigger this process here." + exit 0 + fi + BRANCH=$NEW_BRANCH + DIFF_DIRECTORY=update_diffs + DIFF_FILE=${DIFF_DIRECTORY}/diff_before_upgrade_to_legacy_$(date +"%Y-%m-%d-%H-%M-%S") + mv diff_before_upgrade* ${DIFF_DIRECTORY}/ 2> /dev/null + if ! git diff-index --quiet HEAD; then + echo -e "\e[32mSaving diff to ${DIFF_FILE}...\e[0m" + mkdir -p ${DIFF_DIRECTORY} + git diff "${BRANCH}" --stat > "${DIFF_FILE}" + git diff "${BRANCH}" >> "${DIFF_FILE}" + fi + git fetch origin + git checkout -f "${BRANCH}" +fi + +if [ ! "$DEV" ]; then + echo -e "\e[32mChecking for newer update script...\e[0m" + SHA1_1="$(sha1sum update.sh)" + git fetch origin #${BRANCH} + git checkout "origin/${BRANCH}" update.sh + SHA1_2=$(sha1sum update.sh) + if [[ "${SHA1_1}" != "${SHA1_2}" ]]; then + echo "update.sh changed, please run this script again, exiting." + chmod +x update.sh + exit 2 + fi +fi + +if [ ! "$FORCE" ]; then + read -r -p "Are you sure you want to update mailcow: dockerized? All containers will be stopped. [y/N] " response + if [[ ! "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + echo "OK, exiting." + exit 0 + fi + detect_major_update + migrate_docker_nat +fi + +remove_obsolete_nginx_ports + +echo -e "\e[32mValidating docker-compose stack configuration...\e[0m" +sed -i 's/HTTPS_BIND:-:/HTTPS_BIND:-/g' docker-compose.yml +sed -i 's/HTTP_BIND:-:/HTTP_BIND:-/g' docker-compose.yml +if ! $COMPOSE_COMMAND config -q; then + echo -e "\e[31m\nOh no, something went wrong. Please check the error message above.\e[0m" + exit 1 +fi + +echo -e "\e[32mChecking for conflicting bridges...\e[0m" +MAILCOW_BRIDGE=$($COMPOSE_COMMAND config | grep -i com.docker.network.bridge.name | cut -d':' -f2) +while read NAT_ID; do + iptables -t nat -D POSTROUTING "$NAT_ID" +done < <(iptables -L -vn -t nat --line-numbers | grep "$IPV4_NETWORK" | grep -E 'MASQUERADE.*all' | grep -v "${MAILCOW_BRIDGE}" | cut -d' ' -f1) + +DIFF_DIRECTORY=update_diffs +DIFF_FILE=${DIFF_DIRECTORY}/diff_before_update_$(date +"%Y-%m-%d-%H-%M-%S") +mv diff_before_update* ${DIFF_DIRECTORY}/ 2> /dev/null +if ! git diff-index --quiet HEAD; then + echo -e "\e[32mSaving diff to ${DIFF_FILE}...\e[0m" + mkdir -p ${DIFF_DIRECTORY} + git diff --stat > "${DIFF_FILE}" + git diff >> "${DIFF_FILE}" +fi + +echo -e "\e[32mPrefetching images...\e[0m" +prefetch_images + +echo -e "\e[32mStopping mailcow...\e[0m" +sleep 2 +MAILCOW_CONTAINERS=($($COMPOSE_COMMAND ps -q)) +$COMPOSE_COMMAND down +echo -e "\e[32mChecking for remaining containers...\e[0m" +sleep 2 +for container in "${MAILCOW_CONTAINERS[@]}"; do + docker rm -f "$container" 2> /dev/null +done + +[[ -f data/conf/nginx/ZZZ-ejabberd.conf ]] && rm data/conf/nginx/ZZZ-ejabberd.conf +migrate_solr_config_options +adapt_new_options + +# Silently fixing remote url from andryyy to mailcow +# git remote set-url origin https://github.com/mailcow/mailcow-dockerized + +DEFAULT_REPO="https://github.com/mailcow/mailcow-dockerized" +CURRENT_REPO=$(git config --get remote.origin.url) +if [ "$CURRENT_REPO" != "$DEFAULT_REPO" ]; then + echo "The Repository currently used is not the default Mailcow Repository." + echo "Currently Repository: $CURRENT_REPO" + echo "Default Repository: $DEFAULT_REPO" + if [ ! "$FORCE" ]; then + read -r -p "Should it be changed back to default? [y/N] " repo_response + if [[ "$repo_response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + git remote set-url origin $DEFAULT_REPO + fi + else + echo "Running in forced mode... setting Repo to default!" + git remote set-url origin $DEFAULT_REPO + fi +fi + +if [ ! "$DEV" ]; then + echo -e "\e[32mCommitting current status...\e[0m" + [[ -z "$(git config user.name)" ]] && git config user.name moo + [[ -z "$(git config user.email)" ]] && git config user.email moo@cow.moo + [[ ! -z $(git ls-files data/conf/rspamd/override.d/worker-controller-password.inc) ]] && git rm data/conf/rspamd/override.d/worker-controller-password.inc + git add -u + git commit -am "Before update on ${DATE}" > /dev/null + echo -e "\e[32mFetching updated code from remote...\e[0m" + git fetch origin #${BRANCH} + echo -e "\e[32mMerging local with remote code (recursive, strategy: \"${MERGE_STRATEGY:-theirs}\", options: \"patience\"...\e[0m" + git config merge.defaultToUpstream true + git merge -X"${MERGE_STRATEGY:-theirs}" -Xpatience -m "After update on ${DATE}" + # Need to use a variable to not pass return codes of if checks + MERGE_RETURN=$? + if [[ ${MERGE_RETURN} == 128 ]]; then + echo -e "\e[31m\nOh no, what happened?\n=> You most likely added files to your local mailcow instance that were now added to the official mailcow repository. Please move them to another location before updating mailcow.\e[0m" + exit 1 + elif [[ ${MERGE_RETURN} == 1 ]]; then + echo -e "\e[93mPotential conflict, trying to fix...\e[0m" + git status --porcelain | grep -E "UD|DU" | awk '{print $2}' | xargs rm -v + git add -A + git commit -m "After update on ${DATE}" > /dev/null + git checkout . + echo -e "\e[32mRemoved and recreated files if necessary.\e[0m" + elif [[ ${MERGE_RETURN} != 0 ]]; then + echo -e "\e[31m\nOh no, something went wrong. Please check the error message above.\e[0m" + echo + echo "Run $COMPOSE_COMMAND up -d to restart your stack without updates or try again after fixing the mentioned errors." + exit 1 + fi +elif [ "$DEV" ]; then + echo -e "\e[33mDEVELOPER MODE: Not creating a git diff and commiting it to prevent development stuff within a backup diff...\e[0m" +fi + +echo -e "\e[32mFetching new images, if any...\e[0m" +sleep 2 +$COMPOSE_COMMAND pull + +# Fix missing SSL, does not overwrite existing files +[[ ! -d data/assets/ssl ]] && mkdir -p data/assets/ssl +cp -n -d data/assets/ssl-example/*.pem data/assets/ssl/ + +echo -e "Checking IPv6 settings... " +if grep -q 'SYSCTL_IPV6_DISABLED=1' mailcow.conf; then + echo + echo '!! IMPORTANT !!' + echo + echo 'SYSCTL_IPV6_DISABLED was removed due to complications. IPv6 can be disabled by editing "docker-compose.yml" and setting "enable_ipv6: true" to "enable_ipv6: false".' + echo "This setting will only be active after a complete shutdown of mailcow by running $COMPOSE_COMMAND down followed by $COMPOSE_COMMAND up -d." + echo + echo '!! IMPORTANT !!' + echo + read -p "Press any key to continue..." < /dev/tty +fi + +# Checking for old project name bug +sed -i --follow-symlinks 's#COMPOSEPROJECT_NAME#COMPOSE_PROJECT_NAME#g' mailcow.conf + +# Fix Rspamd maps +if [ -f data/conf/rspamd/custom/global_from_blacklist.map ]; then + mv data/conf/rspamd/custom/global_from_blacklist.map data/conf/rspamd/custom/global_smtp_from_blacklist.map +fi +if [ -f data/conf/rspamd/custom/global_from_whitelist.map ]; then + mv data/conf/rspamd/custom/global_from_whitelist.map data/conf/rspamd/custom/global_smtp_from_whitelist.map +fi + +# Fix deprecated metrics.conf +if [ -f "data/conf/rspamd/local.d/metrics.conf" ]; then + if [ ! -z "$(git diff --name-only origin/master data/conf/rspamd/local.d/metrics.conf)" ]; then + echo -e "\e[33mWARNING\e[0m - Please migrate your customizations of data/conf/rspamd/local.d/metrics.conf to actions.conf and groups.conf after this update." + echo "The deprecated configuration file metrics.conf will be moved to metrics.conf_deprecated after updating mailcow." + fi + mv data/conf/rspamd/local.d/metrics.conf data/conf/rspamd/local.d/metrics.conf_deprecated +fi + +# Set app_info.inc.php +if [ ${BRANCH} == "master" ]; then + mailcow_git_version=$(git describe --tags $(git rev-list --tags --max-count=1)) +elif [ ${BRANCH} == "nightly" ]; then + mailcow_git_version=$(git rev-parse --short $(git rev-parse @{upstream})) + mailcow_last_git_version="" +else + mailcow_git_version=$(git rev-parse --short HEAD) + mailcow_last_git_version="" +fi + +mailcow_git_commit=$(git rev-parse "origin/${BRANCH}") +mailcow_git_commit_date=$(git log -1 --format=%ci @{upstream} ) + +if [ $? -eq 0 ]; then + echo ' data/web/inc/app_info.inc.php + echo ' $MAILCOW_GIT_VERSION="'$mailcow_git_version'";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_LAST_GIT_VERSION="";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_GIT_OWNER="mailcow";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_GIT_REPO="mailcow-dockerized";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_GIT_URL="https://github.com/mailcow/mailcow-dockerized";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_GIT_COMMIT="'$mailcow_git_commit'";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_GIT_COMMIT_DATE="'$mailcow_git_commit_date'";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_BRANCH="'$BRANCH'";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_UPDATEDAT='$(date +%s)';' >> data/web/inc/app_info.inc.php + echo '?>' >> data/web/inc/app_info.inc.php +else + echo ' data/web/inc/app_info.inc.php + echo ' $MAILCOW_GIT_VERSION="'$mailcow_git_version'";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_LAST_GIT_VERSION="";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_GIT_OWNER="mailcow";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_GIT_REPO="mailcow-dockerized";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_GIT_URL="https://github.com/mailcow/mailcow-dockerized";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_GIT_COMMIT="";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_GIT_COMMIT_DATE="";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_BRANCH="'$BRANCH'";' >> data/web/inc/app_info.inc.php + echo ' $MAILCOW_UPDATEDAT='$(date +%s)';' >> data/web/inc/app_info.inc.php + echo '?>' >> data/web/inc/app_info.inc.php + echo -e "\e[33mCannot determine current git repository version...\e[0m" +fi + +if [[ ${SKIP_START} == "y" ]]; then + echo -e "\e[33mNot starting mailcow, please run \"$COMPOSE_COMMAND up -d --remove-orphans\" to start mailcow.\e[0m" +else + echo -e "\e[32mStarting mailcow...\e[0m" + sleep 2 + $COMPOSE_COMMAND up -d --remove-orphans +fi + +echo -e "\e[32mCollecting garbage...\e[0m" +docker_garbage + +# Run post-update-hook +if [ -f "${SCRIPT_DIR}/post_update_hook.sh" ]; then + bash "${SCRIPT_DIR}/post_update_hook.sh" +fi + +# echo "In case you encounter any problem, hard-reset to a state before updating mailcow:" +# echo +# git reflog --color=always | grep "Before update on " +# echo +# echo "Use \"git reset --hard hash-on-the-left\" and run $COMPOSE_COMMAND up -d afterwards." diff --git a/nextcloud/.example-env b/nextcloud/.example-env new file mode 100644 index 0000000..56799e6 --- /dev/null +++ b/nextcloud/.example-env @@ -0,0 +1,10 @@ +# Nextcloud +DOMAIN= +NEXTCLOUD_ADMIN_PASSWORD= +NEXTCLOUD_ADMIN_USER= +NEXTCLOUD_DB_HOST= +NEXTCLOUD_DB_NAME= +NEXTCLOUD_DB_PASSWORD= +NEXTCLOUD_DB_USER= +NEXTCLOUD_TRUSTED_DOMAINS= +ONLYOFFICE_JWT_SECRET= diff --git a/nextcloud/create_infra.bash b/nextcloud/create_infra.bash new file mode 100755 index 0000000..258e8c9 --- /dev/null +++ b/nextcloud/create_infra.bash @@ -0,0 +1,8 @@ +#!/bin/bash + +source .env +source ../core/.env + +docker exec postgres psql -U $POSTGRES_USER -d postgres -c "CREATE USER $NEXTCLOUD_DB_USER WITH PASSWORD '$NEXTCLOUD_DB_PASSWORD';" +docker exec postgres psql -U $POSTGRES_USER -d postgres -c "CREATE DATABASE $NEXTCLOUD_DB_NAME OWNER $NEXTCLOUD_DB_USER;" +docker exec postgres psql -U $POSTGRES_USER -d $NEXTCLOUD_DB_NAME -c "GRANT ALL PRIVILEGES ON DATABASE $NEXTCLOUD_DB_NAME TO $NEXTCLOUD_DB_USER;" diff --git a/nextcloud/custom/custom-config.php b/nextcloud/custom/custom-config.php new file mode 100644 index 0000000..f7e0d30 --- /dev/null +++ b/nextcloud/custom/custom-config.php @@ -0,0 +1,14 @@ + 1, + 'memcache.local' => '\OC\Memcache\APCu', + 'memcache.locking' => '\OC\Memcache\Redis', + 'redis' => [ + 'host' => 'redis', + 'port' => 6379, + ], + 'default_phone_region' => 'DE', + 'hsts' => true, + 'hstsMaxAge' => 15552000, + 'hstsIncludeSubdomains' => true, +); diff --git a/nextcloud/docker-compose.yml b/nextcloud/docker-compose.yml new file mode 100644 index 0000000..dd5871d --- /dev/null +++ b/nextcloud/docker-compose.yml @@ -0,0 +1,80 @@ +# Nextcloud-Stack +services: + nextcloud: + image: nextcloud:31.0-fpm + container_name: nextcloud + depends_on: + - nextcloud-redis + environment: + - NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER:-admin} + - NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD:-admin} + - NEXTCLOUD_DEFAULT_PHONE_REGION=DE + - NEXTCLOUD_TRUSTED_DOMAINS=${NEXTCLOUD_TRUSTED_DOMAINS} + - ONLYOFFICE_JWT_SECRET=${ONLYOFFICE_JWT_SECRET} + - POSTGRES_DB=${NEXTCLOUD_DB_NAME} + - POSTGRES_HOST=${NEXTCLOUD_DB_HOST} + - POSTGRES_PASSWORD=${NEXTCLOUD_DB_PASSWORD} + - POSTGRES_USER=${NEXTCLOUD_DB_USER} + - OVERWRITEPROTOCOL=https + - OVERWRITEHOST=${NEXTCLOUD_DOMAIN} + - REDIS_HOST=nextcloud-redis + - TRUSTED_PROXIES=traefik + labels: + - "traefik.enable=false" + volumes: + - nextcloud-data:/var/www/html + #- ./hooks/post-installation:/docker-entrypoint-hooks.d/post-installation + expose: + - 80 + - 9000 + networks: + - nextcloud + - traefik + - onlyoffice + - database + restart: unless-stopped + + nextcloud-reverse-proxy: + container_name: nextcloud-reverse-proxy + image: nginx:1.27 + depends_on: + - nextcloud + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik" + - "traefik.http.routers.nextcloud-reverse-proxy.rule=Host(`${NEXTCLOUD_DOMAIN}`)" + - "traefik.http.routers.nextcloud-reverse-proxy.entrypoints=web,websecure" + - "traefik.http.routers.nextcloud-reverse-proxy.middlewares=https-redirect" + - "traefik.http.routers.nextcloud-reverse-proxy.tls=true" + - "traefik.http.routers.nextcloud-reverse-proxy.tls.certresolver=le" + - "traefik.http.services.nextcloud-reverse-proxy.loadbalancer.server.port=80" + volumes: + - ./reverse-proxy/nginx.conf:/etc/nginx/nginx.conf + - nextcloud-data:/var/www/html + networks: + - nextcloud + - traefik + - onlyoffice + restart: unless-stopped + + nextcloud-redis: + image: redis:alpine + container_name: nextcloud-redis + networks: + - nextcloud + restart: unless-stopped + +volumes: + nextcloud-data: + name: nextcloud-data + +networks: + database: + external: true + nextcloud: + name: nextcloud + external: true + traefik: + external: true + onlyoffice: + external: true diff --git a/nextcloud/hooks/post-installation/install-drawio.sh b/nextcloud/hooks/post-installation/install-drawio.sh new file mode 100644 index 0000000..c0ec041 --- /dev/null +++ b/nextcloud/hooks/post-installation/install-drawio.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -e + +echo "Installing Draw.io plugin..." +php /var/www/html/occ app:install drawio +echo "Draw.io plugin installed successfully!" \ No newline at end of file diff --git a/nextcloud/hooks/post-installation/install-only-office.sh b/nextcloud/hooks/post-installation/install-only-office.sh new file mode 100644 index 0000000..0017fa9 --- /dev/null +++ b/nextcloud/hooks/post-installation/install-only-office.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Print all commands and their arguments as they are executed. +set -x + +# Get the trusted domains from the Nextcloud config +php /var/www/html/occ --no-warnings config:system:get trusted_domains >> trusted_domain.tmp + +# If the nextcloud-reverse-proxy domain is not in the trusted domains, add it +if ! grep -q "nextcloud-reverse-proxy" trusted_domain.tmp; then + # Get the number of trusted domains + TRUSTED_INDEX=$(cat trusted_domain.tmp | wc -l); + # Add the nextcloud-reverse-proxy domain to the trusted domains + php /var/www/html/occ --no-warnings config:system:set trusted_domains $TRUSTED_INDEX --value="nextcloud-reverse-proxy" +fi + +rm trusted_domain.tmp + +# Install OnlyOffice app +php /var/www/html/occ --no-warnings app:install onlyoffice + +# Set basic OnlyOffice configuration +# Set the DocumentServerUrl to the path of the OnlyOffice Document Server +php /var/www/html/occ --no-warnings config:system:set onlyoffice DocumentServerUrl --value="/ds-vpath/" +# Set the DocumentServerInternalUrl to the URL of the OnlyOffice Document Server +php /var/www/html/occ --no-warnings config:system:set onlyoffice DocumentServerInternalUrl --value="http://onlyoffice-document-server/" +# Set the StorageUrl to the URL of the Nextcloud Reverse Proxy +php /var/www/html/occ --no-warnings config:system:set onlyoffice StorageUrl --value="http://nextcloud-reverse-proxy/" +# Set the JWT secret +echo ${ONLYOFFICE_JWT_SECRET} +php /var/www/html/occ --no-warnings config:system:set onlyoffice jwt_secret --value="${ONLYOFFICE_JWT_SECRET}" diff --git a/nextcloud/reverse-proxy/nginx.conf b/nextcloud/reverse-proxy/nginx.conf new file mode 100644 index 0000000..e664212 --- /dev/null +++ b/nextcloud/reverse-proxy/nginx.conf @@ -0,0 +1,144 @@ +user www-data; +worker_processes 1; + +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + + upstream backend { + server nextcloud:9000; + } + + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + map $http_host $this_host { + "" $host; + default $http_host; + } + + map $http_x_forwarded_proto $the_scheme { + default $http_x_forwarded_proto; + "" $scheme; + } + + map $http_x_forwarded_host $the_host { + default $http_x_forwarded_host; + "" $this_host; + } + + server { + listen 80; + # The below allows for being behind a reverse proxy and allowing the Nextcloud app to connect + server_tokens off; + + # Add headers to serve security related headers + add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;"; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + add_header X-Robots-Tag none; + add_header X-Download-Options noopen; + add_header X-Permitted-Cross-Domain-Policies none; + + root /var/www/html; + client_max_body_size 10G; # 0=unlimited - set max upload size + fastcgi_buffers 64 4K; + + gzip off; + + index index.php; + error_page 403 /core/templates/403.php; + error_page 404 /core/templates/404.php; + + rewrite ^/.well-known/carddav /remote.php/dav/ permanent; + rewrite ^/.well-known/caldav /remote.php/dav/ permanent; + + location = /robots.txt { + allow all; + log_not_found off; + access_log off; + } + + location ~ ^/(build|tests|config|lib|3rdparty|templates|data)/ { + deny all; + } + + location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) { + deny all; + } + + location / { + rewrite ^/remote/(.*) /remote.php last; + rewrite ^(/core/doc/[^\/]+/)$ $1/index.html; + try_files $uri $uri/ =404; + } + + location ~* ^/ds-vpath/ { + rewrite /ds-vpath/(.*) /$1 break; + proxy_pass http://onlyoffice-document-server; + proxy_redirect off; + + client_max_body_size 100m; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $the_host/ds-vpath; + proxy_set_header X-Forwarded-Proto https; + } + + location ~ \.php(?:$|/) { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_param HTTPS on; + fastcgi_param modHeadersAvailable true; #Avoid sending the security headers twice + fastcgi_pass backend; + fastcgi_intercept_errors on; + } + + # Adding the cache control header for js and css files + # Make sure it is BELOW the location ~ \.php(?:$|/) { block + location ~* \.(?:css|js)$ { + add_header Cache-Control "public, max-age=7200"; + # Add headers to serve security related headers + add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;"; + add_header X-Content-Type-Options nosniff; + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-XSS-Protection "1; mode=block"; + add_header X-Robots-Tag none; + add_header X-Download-Options noopen; + add_header X-Permitted-Cross-Domain-Policies none; + # Optional: Don't log access to assets + access_log off; + } + + # Optional: Don't log access to other assets + location ~* \.(?:jpg|jpeg|gif|bmp|ico|png|swf)$ { + access_log off; + } + + } +} diff --git a/onlyoffice/.example-env b/onlyoffice/.example-env new file mode 100644 index 0000000..0c2e617 --- /dev/null +++ b/onlyoffice/.example-env @@ -0,0 +1,8 @@ +ONLYOFFICE_JWT_SECRET= +ONLYOFFICE_DB_HOST=postgres +ONLYOFFICE_DB_NAME= +ONLYOFFICE_DB_PASSWORD= +ONLYOFFICE_DB_PORT=5432 +ONLYOFFICE_DB_TYPE=postgres +ONLYOFFICE_DB_USER= + diff --git a/onlyoffice/.travis.yml b/onlyoffice/.travis.yml new file mode 100644 index 0000000..2e37ca6 --- /dev/null +++ b/onlyoffice/.travis.yml @@ -0,0 +1,131 @@ +language: generic + +dist: trusty + +env: + # community edition + - config: standalone.yml + + # integration edition + - config: standalone.yml + PRODUCT_NAME: documentserver-ie + + + # certificates (default tls if onlyoffice not exists) + - config: certs.yml + ssl: true + + # certificates (default onlyoffice if exists) + - config: certs.yml + ssl: true + private_key: onlyoffice.key + certificate_request: onlyoffice.csr + certificate: onlyoffice.crt + + # custom certificates + - config: certs-customized.yml + ssl: true + private_key: mycert.key + certificate_request: mycert.csr + certificate: mycert.crt + SSL_CERTIFICATE_PATH: /var/www/onlyoffice/Data/certs/mycert.crt + SSL_KEY_PATH: /var/www/onlyoffice/Data/certs/mycert.key + + + # postgresql 16 + - config: postgres.yml + POSTGRES_VERSION: 16 + + # postgresql 15 + - config: postgres.yml + POSTGRES_VERSION: 15 + + # postgresql 14 + - config: postgres.yml + POSTGRES_VERSION: 14 + + # postgresql 13 + - config: postgres.yml + POSTGRES_VERSION: 13 + + # postgresql 12 + - config: postgres.yml + + # postgresql custom values + - config: postgres.yml + DB_NAME: mydb + DB_USER: myuser + DB_PWD: password + POSTGRES_DB: mydb + POSTGRES_USER: myuser + + # postgresql deprecated variables + - config: postgres-old.yml + + + # mysql 8 + - config: mysql.yml + MYSQL_VERSION: 8 + + # mysql 5 + - config: mysql.yml + MYSQL_VERSION: 5 + + # mysql 5.7 + - config: mysql.yml + + + # mariadb 10 + - config: mariadb.yml + MARIADB_VERSION: 10 + + # mariadb 10.5 + - config: mariadb.yml + + + - config: activemq.yml + ACTIVEMQ_VERSION: latest + + # activemq 5.14.3 + - config: activemq.yml + + + # rabbitmq latest + - config: rabbitmq.yml + + # rabbitmq 3 + - config: rabbitmq.yml + RABBITMQ_VERSION: 3 + + # rabbitmq old variables + - config: rabbitmq-old.yml + + + # redis latest with community edition + - config: redis.yml + + # redis latest with integraion edition + - config: redis.yml + PRODUCT_NAME: documentserver-ie + + # redis 6 + - config: redis.yml + REDIS_VERSION: 6 + + # redis 5 + - config: redis.yml + REDIS_VERSION: 5 + + + # graphite + - config: graphite.yml + +services: + - docker + +script: + # Go to tests dir + - cd ${PWD}/tests + + # Run test. + - ./test.sh diff --git a/onlyoffice/Dockerfile b/onlyoffice/Dockerfile new file mode 100644 index 0000000..63f140c --- /dev/null +++ b/onlyoffice/Dockerfile @@ -0,0 +1,138 @@ +ARG BASE_VERSION=24.04 + +ARG BASE_IMAGE=ubuntu:$BASE_VERSION + +FROM ${BASE_IMAGE} AS documentserver +LABEL maintainer Ascensio System SIA + +ARG BASE_VERSION +ARG PG_VERSION=16 +ARG PACKAGE_SUFFIX=t64 + +ENV OC_RELEASE_NUM=21 +ENV OC_RU_VER=12 +ENV OC_RU_REVISION_VER=0 +ENV OC_RESERVED_NUM=0 +ENV OC_RU_DATE=0 +ENV OC_PATH=${OC_RELEASE_NUM}${OC_RU_VER}000 +ENV OC_FILE_SUFFIX=${OC_RELEASE_NUM}.${OC_RU_VER}.${OC_RU_REVISION_VER}.${OC_RESERVED_NUM}.${OC_RU_DATE}${OC_FILE_SUFFIX}dbru +ENV OC_VER_DIR=${OC_RELEASE_NUM}_${OC_RU_VER} +ENV OC_DOWNLOAD_URL=https://download.oracle.com/otn_software/linux/instantclient/${OC_PATH} + +ENV LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=en_US.UTF-8 DEBIAN_FRONTEND=noninteractive PG_VERSION=${PG_VERSION} BASE_VERSION=${BASE_VERSION} + +ARG ONLYOFFICE_VALUE=onlyoffice + +RUN echo "#!/bin/sh\nexit 0" > /usr/sbin/policy-rc.d && \ + apt-get -y update && \ + apt-get -yq install wget apt-transport-https gnupg locales lsb-release && \ + wget -q -O /etc/apt/sources.list.d/mssql-release.list "https://packages.microsoft.com/config/ubuntu/$BASE_VERSION/prod.list" && \ + wget -q -O /tmp/microsoft.asc https://packages.microsoft.com/keys/microsoft.asc && \ + apt-key add /tmp/microsoft.asc && \ + gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg < /tmp/microsoft.asc && \ + apt-get -y update && \ + locale-gen en_US.UTF-8 && \ + echo ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true | debconf-set-selections && \ + ACCEPT_EULA=Y apt-get -yq install \ + adduser \ + apt-utils \ + bomstrip \ + certbot \ + cron \ + curl \ + htop \ + libaio1${PACKAGE_SUFFIX} \ + libasound2${PACKAGE_SUFFIX} \ + libboost-regex-dev \ + libcairo2 \ + libcurl3-gnutls \ + libcurl4 \ + libgtk-3-0 \ + libnspr4 \ + libnss3 \ + libstdc++6 \ + libxml2 \ + libxss1 \ + libxtst6 \ + mssql-tools18 \ + mysql-client \ + nano \ + net-tools \ + netcat-openbsd \ + nginx-extras \ + postgresql \ + postgresql-client \ + pwgen \ + rabbitmq-server \ + redis-server \ + sudo \ + supervisor \ + ttf-mscorefonts-installer \ + unixodbc-dev \ + unzip \ + xvfb \ + xxd \ + zlib1g || dpkg --configure -a && \ + # Added dpkg --configure -a to handle installation issues with rabbitmq-server on arm64 architecture + if [ $(ls -l /usr/share/fonts/truetype/msttcorefonts | wc -l) -ne 61 ]; \ + then echo 'msttcorefonts failed to download'; exit 1; fi && \ + echo "SERVER_ADDITIONAL_ERL_ARGS=\"+S 1:1\"" | tee -a /etc/rabbitmq/rabbitmq-env.conf && \ + sed -i "s/bind .*/bind 127.0.0.1/g" /etc/redis/redis.conf && \ + sed 's|\(application\/zip.*\)|\1\n application\/wasm wasm;|' -i /etc/nginx/mime.types && \ + pg_conftool $PG_VERSION main set listen_addresses 'localhost' && \ + service postgresql restart && \ + sudo -u postgres psql -c "CREATE USER $ONLYOFFICE_VALUE WITH password '$ONLYOFFICE_VALUE';" && \ + sudo -u postgres psql -c "CREATE DATABASE $ONLYOFFICE_VALUE OWNER $ONLYOFFICE_VALUE;" && \ + wget -O basic.zip ${OC_DOWNLOAD_URL}/instantclient-basic-linux.x64-${OC_FILE_SUFFIX}.zip && \ + wget -O sqlplus.zip ${OC_DOWNLOAD_URL}/instantclient-sqlplus-linux.x64-${OC_FILE_SUFFIX}.zip && \ + unzip -d /usr/share basic.zip && \ + unzip -d /usr/share sqlplus.zip && \ + mv /usr/share/instantclient_${OC_VER_DIR} /usr/share/instantclient && \ + service postgresql stop && \ + service redis-server stop && \ + service rabbitmq-server stop && \ + service supervisor stop && \ + service nginx stop && \ + rm -rf /var/lib/apt/lists/* + +COPY config/supervisor/supervisor /etc/init.d/ +COPY config/supervisor/ds/*.conf /etc/supervisor/conf.d/ +COPY run-document-server.sh /app/ds/run-document-server.sh +COPY oracle/sqlplus /usr/bin/sqlplus + +EXPOSE 80 443 + +ARG COMPANY_NAME=onlyoffice +ARG PRODUCT_NAME=documentserver +ARG PRODUCT_EDITION= +ARG PACKAGE_VERSION= +ARG TARGETARCH +ARG PACKAGE_BASEURL="http://download.onlyoffice.com/install/documentserver/linux" + +ENV COMPANY_NAME=$COMPANY_NAME \ + PRODUCT_NAME=$PRODUCT_NAME \ + PRODUCT_EDITION=$PRODUCT_EDITION \ + DS_PLUGIN_INSTALLATION=false \ + DS_DOCKER_INSTALLATION=true + +RUN PACKAGE_FILE="${COMPANY_NAME}-${PRODUCT_NAME}${PRODUCT_EDITION}${PACKAGE_VERSION:+_$PACKAGE_VERSION}_${TARGETARCH:-$(dpkg --print-architecture)}.deb" && \ + wget -q -P /tmp "$PACKAGE_BASEURL/$PACKAGE_FILE" && \ + apt-get -y update && \ + service postgresql start && \ + apt-get -yq install /tmp/$PACKAGE_FILE && \ + service postgresql stop && \ + chmod 755 /etc/init.d/supervisor && \ + sed "s/COMPANY_NAME/${COMPANY_NAME}/g" -i /etc/supervisor/conf.d/*.conf && \ + service supervisor stop && \ + chmod 755 /app/ds/*.sh && \ + printf "\nGO" >> "/var/www/$COMPANY_NAME/documentserver/server/schema/mssql/createdb.sql" && \ + printf "\nGO" >> "/var/www/$COMPANY_NAME/documentserver/server/schema/mssql/removetbl.sql" && \ + printf "\nexit" >> "/var/www/$COMPANY_NAME/documentserver/server/schema/oracle/createdb.sql" && \ + printf "\nexit" >> "/var/www/$COMPANY_NAME/documentserver/server/schema/oracle/removetbl.sql" && \ + rm -f /tmp/$PACKAGE_FILE && \ + rm -rf /var/log/$COMPANY_NAME && \ + rm -rf /var/lib/apt/lists/* + +VOLUME /var/log/$COMPANY_NAME /var/lib/$COMPANY_NAME /var/www/$COMPANY_NAME/Data /var/lib/postgresql /var/lib/rabbitmq /var/lib/redis /usr/share/fonts/truetype/custom + +ENTRYPOINT ["/app/ds/run-document-server.sh"] diff --git a/onlyoffice/LICENSE.txt b/onlyoffice/LICENSE.txt new file mode 100644 index 0000000..dba13ed --- /dev/null +++ b/onlyoffice/LICENSE.txt @@ -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/onlyoffice/Makefile b/onlyoffice/Makefile new file mode 100644 index 0000000..bb80cde --- /dev/null +++ b/onlyoffice/Makefile @@ -0,0 +1,66 @@ +COMPANY_NAME ?= ONLYOFFICE +GIT_BRANCH ?= develop +PRODUCT_NAME ?= documentserver +PRODUCT_EDITION ?= +PRODUCT_VERSION ?= 0.0.0 +BUILD_NUMBER ?= 0 +BUILD_CHANNEL ?= nightly +ONLYOFFICE_VALUE ?= onlyoffice + +COMPANY_NAME_LOW = $(shell echo $(COMPANY_NAME) | tr A-Z a-z) + +PACKAGE_NAME := $(COMPANY_NAME_LOW)-$(PRODUCT_NAME)$(PRODUCT_EDITION) +PACKAGE_VERSION ?= $(PRODUCT_VERSION)-$(BUILD_NUMBER)~stretch +PACKAGE_BASEURL ?= https://s3.eu-west-1.amazonaws.com/repo-doc-onlyoffice-com/server/linux/debian + +ifeq ($(BUILD_CHANNEL),$(filter $(BUILD_CHANNEL),nightly test)) + DOCKER_TAG := $(PRODUCT_VERSION).$(BUILD_NUMBER) +else + DOCKER_TAG := $(PRODUCT_VERSION).$(BUILD_NUMBER)-$(subst /,-,$(GIT_BRANCH)) +endif + +DOCKER_ORG ?= $(COMPANY_NAME_LOW) +DOCKER_IMAGE := $(DOCKER_ORG)/4testing-$(PRODUCT_NAME)$(PRODUCT_EDITION) +DOCKER_DUMMY := $(COMPANY_NAME_LOW)-$(PRODUCT_NAME)$(PRODUCT_EDITION)__$(DOCKER_TAG).dummy +DOCKER_ARCH := $(COMPANY_NAME_LOW)-$(PRODUCT_NAME)_$(DOCKER_TAG).tar.gz + +.PHONY: all clean clean-docker image deploy docker + +$(DOCKER_DUMMY): + docker pull ubuntu:22.04 + docker build \ + --build-arg COMPANY_NAME=$(COMPANY_NAME_LOW) \ + --build-arg PRODUCT_NAME=$(PRODUCT_NAME) \ + --build-arg PRODUCT_EDITION=$(PRODUCT_EDITION) \ + --build-arg PACKAGE_VERSION=$(PACKAGE_VERSION) \ + --build-arg PACKAGE_BASEURL=$(PACKAGE_BASEURL) \ + --build-arg TARGETARCH=amd64 \ + --build-arg ONLYOFFICE_VALUE=$(ONLYOFFICE_VALUE) \ + -t $(DOCKER_IMAGE):$(DOCKER_TAG) . && \ + mkdir -p $$(dirname $@) && \ + echo "Done" > $@ + +$(DOCKER_ARCH): $(DOCKER_DUMMY) + docker save $(DOCKER_IMAGE):$(DOCKER_TAG) | \ + gzip > $@ + +all: image + +clean: + rm -rfv *.dummy *.tar.gz + +clean-docker: + docker rmi -f $$(docker images -q $(COMPANY_NAME_LOW)/*) || exit 0 + +image: $(DOCKER_DUMMY) + +deploy: $(DOCKER_DUMMY) + for i in {1..3}; do \ + docker push $(DOCKER_IMAGE):$(DOCKER_TAG) && break || sleep 1m; \ + done +ifeq ($(BUILD_CHANNEL),nightly) + docker tag $(DOCKER_IMAGE):$(DOCKER_TAG) $(DOCKER_IMAGE):latest + for i in {1..3}; do \ + docker push $(DOCKER_IMAGE):latest && break || sleep 1m; \ + done +endif diff --git a/onlyoffice/README.md b/onlyoffice/README.md new file mode 100644 index 0000000..298fe09 --- /dev/null +++ b/onlyoffice/README.md @@ -0,0 +1,387 @@ +* [Overview](#overview) +* [Functionality](#functionality) +* [Recommended System Requirements](#recommended-system-requirements) +* [Running Docker Image](#running-docker-image) +* [Configuring Docker Image](#configuring-docker-image) + - [Storing Data](#storing-data) + - [Running ONLYOFFICE Document Server on Different Port](#running-onlyoffice-document-server-on-different-port) + - [Running ONLYOFFICE Document Server using HTTPS](#running-onlyoffice-document-server-using-https) + + [Generation of Self Signed Certificates](#generation-of-self-signed-certificates) + + [Strengthening the Server Security](#strengthening-the-server-security) + + [Installation of the SSL Certificates](#installation-of-the-ssl-certificates) + + [Available Configuration Parameters](#available-configuration-parameters) +* [Installing ONLYOFFICE Document Server integrated with Community and Mail Servers](#installing-onlyoffice-document-server-integrated-with-community-and-mail-servers) +* [ONLYOFFICE Document Server ipv6 setup](#onlyoffice-document-server-ipv6-setup) +* [Issues](#issues) + - [Docker Issues](#docker-issues) + - [Document Server usage Issues](#document-server-usage-issues) +* [Project Information](#project-information) +* [User Feedback and Support](#user-feedback-and-support) + +## Overview + +ONLYOFFICE Document Server is an online office suite comprising viewers and editors for texts, spreadsheets and presentations, fully compatible with Office Open XML formats: .docx, .xlsx, .pptx and enabling collaborative editing in real time. + +Starting from version 6.0, Document Server is distributed as ONLYOFFICE Docs. It has [three editions](https://github.com/ONLYOFFICE/DocumentServer#onlyoffice-document-server-editions). With this image, you will install the free Community version. + +ONLYOFFICE Docs can be used as a part of ONLYOFFICE Workspace or with third-party sync&share solutions (e.g. Nextcloud, ownCloud, Seafile) to enable collaborative editing within their interface. + +***Important*** Please update `docker-engine` to latest version (`20.10.21` as of writing this doc) before using it. We use `ubuntu:22.04` as base image and it older versions of docker have compatibility problems with it + +## Functionality ## +* ONLYOFFICE Document Editor +* ONLYOFFICE Spreadsheet Editor +* ONLYOFFICE Presentation Editor +* ONLYOFFICE Documents application for iOS +* Collaborative editing +* Hieroglyph support +* Support for all the popular formats: DOC, DOCX, TXT, ODT, RTF, ODP, EPUB, ODS, XLS, XLSX, CSV, PPTX, HTML + +Integrating it with ONLYOFFICE Community Server you will be able to: +* view and edit files stored on Drive, Box, Dropbox, OneDrive, OwnCloud connected to ONLYOFFICE; +* share files; +* embed documents on a website; +* manage access rights to documents. + +## Recommended System Requirements + +* **RAM**: 4 GB or more +* **CPU**: dual-core 2 GHz or higher +* **Swap**: at least 2 GB +* **HDD**: at least 2 GB of free space +* **Distribution**: 64-bit Red Hat, CentOS or other compatible distributive with kernel version 3.8 or later, 64-bit Debian, Ubuntu or other compatible distributive with kernel version 3.8 or later +* **Docker**: version 1.9.0 or later + +## Running Docker Image + + sudo docker run -i -t -d -p 80:80 onlyoffice/documentserver + +Use this command if you wish to install ONLYOFFICE Document Server separately. To install ONLYOFFICE Document Server integrated with Community and Mail Servers, refer to the corresponding instructions below. + +## Configuring Docker Image + +### Storing Data + +All the data are stored in the specially-designated directories, **data volumes**, at the following location: +* **/var/log/onlyoffice** for ONLYOFFICE Document Server logs +* **/var/www/onlyoffice/Data** for certificates +* **/var/lib/onlyoffice** for file cache +* **/var/lib/postgresql** for database + +To get access to your data from outside the container, you need to mount the volumes. It can be done by specifying the '-v' option in the docker run command. + + sudo docker run -i -t -d -p 80:80 \ + -v /app/onlyoffice/DocumentServer/logs:/var/log/onlyoffice \ + -v /app/onlyoffice/DocumentServer/data:/var/www/onlyoffice/Data \ + -v /app/onlyoffice/DocumentServer/lib:/var/lib/onlyoffice \ + -v /app/onlyoffice/DocumentServer/rabbitmq:/var/lib/rabbitmq \ + -v /app/onlyoffice/DocumentServer/redis:/var/lib/redis \ + -v /app/onlyoffice/DocumentServer/db:/var/lib/postgresql onlyoffice/documentserver + +Normally, you do not need to store container data because the container's operation does not depend on its state. Saving data will be useful: +* For easy access to container data, such as logs +* To remove the limit on the size of the data inside the container +* When using services launched outside the container such as PostgreSQL, Redis, RabbitMQ + +### Running ONLYOFFICE Document Server on Different Port + +To change the port, use the -p command. E.g.: to make your portal accessible via port 8080 execute the following command: + + sudo docker run -i -t -d -p 8080:80 onlyoffice/documentserver + +### Running ONLYOFFICE Document Server using HTTPS + + sudo docker run -i -t -d -p 443:443 \ + -v /app/onlyoffice/DocumentServer/data:/var/www/onlyoffice/Data onlyoffice/documentserver + +Access to the onlyoffice application can be secured using SSL so as to prevent unauthorized access. While a CA certified SSL certificate allows for verification of trust via the CA, a self signed certificates can also provide an equal level of trust verification as long as each client takes some additional steps to verify the identity of your website. Below the instructions on achieving this are provided. + +To secure the application via SSL basically two things are needed: + +- **Private key (.key)** +- **SSL certificate (.crt)** + +So you need to create and install the following files: + + /app/onlyoffice/DocumentServer/data/certs/tls.key + /app/onlyoffice/DocumentServer/data/certs/tls.crt + +When using CA certified certificates (e.g [Let's encrypt](https://letsencrypt.org)), these files are provided to you by the CA. If you are using self-signed certificates you need to generate these files [yourself](#generation-of-self-signed-certificates). + +#### Using the automatically generated Let's Encrypt SSL Certificates + + sudo docker run -i -t -d -p 80:80 -p 443:443 \ + -e LETS_ENCRYPT_DOMAIN=your_domain -e LETS_ENCRYPT_MAIL=your_mail onlyoffice/documentserver + +If you want to get and extend Let's Encrypt SSL Certificates automatically just set LETS_ENCRYPT_DOMAIN and LETS_ENCRYPT_MAIL variables. + +#### Generation of Self Signed Certificates + +Generation of self-signed SSL certificates involves a simple 3 step procedure. + +**STEP 1**: Create the server private key + +```bash +openssl genrsa -out tls.key 2048 +``` + +**STEP 2**: Create the certificate signing request (CSR) + +```bash +openssl req -new -key tls.key -out tls.csr +``` + +**STEP 3**: Sign the certificate using the private key and CSR + +```bash +openssl x509 -req -days 365 -in tls.csr -signkey tls.key -out tls.crt +``` + +You have now generated an SSL certificate that's valid for 365 days. + +#### Strengthening the server security + +This section provides you with instructions to [strengthen your server security](https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html). +To achieve this you need to generate stronger DHE parameters. + +```bash +openssl dhparam -out dhparam.pem 2048 +``` + +#### Installation of the SSL Certificates + +Out of the four files generated above, you need to install the `tls.key`, `tls.crt` and `dhparam.pem` files at the onlyoffice server. The CSR file is not needed, but do make sure you safely backup the file (in case you ever need it again). + +The default path that the onlyoffice application is configured to look for the SSL certificates is at `/var/www/onlyoffice/Data/certs`, this can however be changed using the `SSL_KEY_PATH`, `SSL_CERTIFICATE_PATH` and `SSL_DHPARAM_PATH` configuration options. + +The `/var/www/onlyoffice/Data/` path is the path of the data store, which means that you have to create a folder named certs inside `/app/onlyoffice/DocumentServer/data/` and copy the files into it and as a measure of security you will update the permission on the `tls.key` file to only be readable by the owner. + +```bash +mkdir -p /app/onlyoffice/DocumentServer/data/certs +cp tls.key /app/onlyoffice/DocumentServer/data/certs/ +cp tls.crt /app/onlyoffice/DocumentServer/data/certs/ +cp dhparam.pem /app/onlyoffice/DocumentServer/data/certs/ +chmod 400 /app/onlyoffice/DocumentServer/data/certs/tls.key +``` + +You are now just one step away from having our application secured. + +#### Available Configuration Parameters + +*Please refer the docker run command options for the `--env-file` flag where you can specify all required environment variables in a single file. This will save you from writing a potentially long docker run command.* + +Below is the complete list of parameters that can be set using environment variables. + +- **ONLYOFFICE_HTTPS_HSTS_ENABLED**: Advanced configuration option for turning off the HSTS configuration. Applicable only when SSL is in use. Defaults to `true`. +- **ONLYOFFICE_HTTPS_HSTS_MAXAGE**: Advanced configuration option for setting the HSTS max-age in the onlyoffice nginx vHost configuration. Applicable only when SSL is in use. Defaults to `31536000`. +- **SSL_CERTIFICATE_PATH**: The path to the SSL certificate to use. Defaults to `/var/www/onlyoffice/Data/certs/tls.crt`. +- **SSL_KEY_PATH**: The path to the SSL certificate's private key. Defaults to `/var/www/onlyoffice/Data/certs/tls.key`. +- **SSL_DHPARAM_PATH**: The path to the Diffie-Hellman parameter. Defaults to `/var/www/onlyoffice/Data/certs/dhparam.pem`. +- **SSL_VERIFY_CLIENT**: Enable verification of client certificates using the `CA_CERTIFICATES_PATH` file. Defaults to `false` +- **NODE_EXTRA_CA_CERTS**: The [NODE_EXTRA_CA_CERTS](https://nodejs.org/api/cli.html#node_extra_ca_certsfile "Node.js documentation") to extend CAs with the extra certificates for Node.js. Defaults to `/var/www/onlyoffice/Data/certs/extra-ca-certs.pem`. +- **DB_TYPE**: The database type. Supported values are `postgres`, `mariadb`, `mysql`, `mssql` or `oracle`. Defaults to `postgres`. +- **DB_HOST**: The IP address or the name of the host where the database server is running. +- **DB_PORT**: The database server port number. +- **DB_NAME**: The name of a database to use. Should be existing on container startup. +- **DB_USER**: The new user name with superuser permissions for the database account. +- **DB_PWD**: The password set for the database account. +- **AMQP_URI**: The [AMQP URI](https://www.rabbitmq.com/uri-spec.html "RabbitMQ URI Specification") to connect to message broker server. +- **AMQP_TYPE**: The message broker type. Supported values are `rabbitmq` or `activemq`. Defaults to `rabbitmq`. +- **REDIS_SERVER_HOST**: The IP address or the name of the host where the Redis server is running. +- **REDIS_SERVER_PORT**: The Redis server port number. +- **REDIS_SERVER_PASS**: The Redis server password. The password is not set by default. +- **NGINX_WORKER_PROCESSES**: Defines the number of nginx worker processes. +- **NGINX_WORKER_CONNECTIONS**: Sets the maximum number of simultaneous connections that can be opened by a nginx worker process. +- **SECURE_LINK_SECRET**: Defines secret for the nginx config directive [secure_link_md5](https://nginx.org/en/docs/http/ngx_http_secure_link_module.html#secure_link_md5). Defaults to `random string`. +- **JWT_ENABLED**: Specifies the enabling the JSON Web Token validation by the ONLYOFFICE Document Server. Defaults to `true`. +- **JWT_SECRET**: Defines the secret key to validate the JSON Web Token in the request to the ONLYOFFICE Document Server. Defaults to random value. +- **JWT_HEADER**: Defines the http header that will be used to send the JSON Web Token. Defaults to `Authorization`. +- **JWT_IN_BODY**: Specifies the enabling the token validation in the request body to the ONLYOFFICE Document Server. Defaults to `false`. +- **WOPI_ENABLED**: Specifies the enabling the wopi handlers. Defaults to `false`. +- **ALLOW_META_IP_ADDRESS**: Defines if it is allowed to connect meta IP address or not. Defaults to `false`. +- **ALLOW_PRIVATE_IP_ADDRESS**: Defines if it is allowed to connect private IP address or not. Defaults to `false`. +- **USE_UNAUTHORIZED_STORAGE**: Set to `true`if using selfsigned certificates for your storage server e.g. Nextcloud. Defaults to `false` +- **GENERATE_FONTS**: When 'true' regenerates fonts list and the fonts thumbnails etc. at each start. Defaults to `true` +- **METRICS_ENABLED**: Specifies the enabling StatsD for ONLYOFFICE Document Server. Defaults to `false`. +- **METRICS_HOST**: Defines StatsD listening host. Defaults to `localhost`. +- **METRICS_PORT**: Defines StatsD listening port. Defaults to `8125`. +- **METRICS_PREFIX**: Defines StatsD metrics prefix for backend services. Defaults to `ds.`. +- **LETS_ENCRYPT_DOMAIN**: Defines the domain for Let's Encrypt certificate. +- **LETS_ENCRYPT_MAIL**: Defines the domain administator mail address for Let's Encrypt certificate. +- **PLUGINS_ENABLED**: Defines whether to enable default plugins. Defaults to `true`. + +## Installing ONLYOFFICE Document Server integrated with Community and Mail Servers + +ONLYOFFICE Document Server is a part of ONLYOFFICE Community Edition that comprises also Community Server and Mail Server. To install them, follow these easy steps: + +**STEP 1**: Create the `onlyoffice` network. + +```bash +docker network create --driver bridge onlyoffice +``` +Then launch containers on it using the 'docker run --net onlyoffice' option: + +**STEP 2**: Install MySQL. + +Follow [these steps](#installing-mysql) to install MySQL server. + +**STEP 3**: Generate JWT Secret + +JWT secret defines the secret key to validate the JSON Web Token in the request to the **ONLYOFFICE Document Server**. You can specify it yourself or easily get it using the command: +``` +JWT_SECRET=$(cat /dev/urandom | tr -dc A-Za-z0-9 | head -c 12); +``` + +**STEP 4**: Install ONLYOFFICE Document Server. + +```bash +sudo docker run --net onlyoffice -i -t -d --restart=always --name onlyoffice-document-server \ + -e JWT_ENABLED=true \ + -e JWT_SECRET=${JWT_SECRET} \ + -e JWT_HEADER=AuthorizationJwt \ + -v /app/onlyoffice/DocumentServer/logs:/var/log/onlyoffice \ + -v /app/onlyoffice/DocumentServer/data:/var/www/onlyoffice/Data \ + -v /app/onlyoffice/DocumentServer/lib:/var/lib/onlyoffice \ + -v /app/onlyoffice/DocumentServer/db:/var/lib/postgresql \ + onlyoffice/documentserver +``` + +**STEP 5**: Install ONLYOFFICE Mail Server. + +For the mail server correct work you need to specify its hostname 'yourdomain.com'. + +```bash +sudo docker run --init --net onlyoffice --privileged -i -t -d --restart=always --name onlyoffice-mail-server -p 25:25 -p 143:143 -p 587:587 \ + -e MYSQL_SERVER=onlyoffice-mysql-server \ + -e MYSQL_SERVER_PORT=3306 \ + -e MYSQL_ROOT_USER=root \ + -e MYSQL_ROOT_PASSWD=my-secret-pw \ + -e MYSQL_SERVER_DB_NAME=onlyoffice_mailserver \ + -v /app/onlyoffice/MailServer/data:/var/vmail \ + -v /app/onlyoffice/MailServer/data/certs:/etc/pki/tls/mailserver \ + -v /app/onlyoffice/MailServer/logs:/var/log \ + -h yourdomain.com \ + onlyoffice/mailserver +``` + +The additional parameters for mail server are available [here](https://github.com/ONLYOFFICE/Docker-CommunityServer/blob/master/docker-compose.workspace_enterprise.yml#L87). + +To learn more, refer to the [ONLYOFFICE Mail Server documentation](https://github.com/ONLYOFFICE/Docker-MailServer "ONLYOFFICE Mail Server documentation"). + +**STEP 6**: Install ONLYOFFICE Community Server + +```bash +sudo docker run --net onlyoffice -i -t -d --privileged --restart=always --name onlyoffice-community-server -p 80:80 -p 443:443 -p 5222:5222 --cgroupns=host \ + -e MYSQL_SERVER_ROOT_PASSWORD=my-secret-pw \ + -e MYSQL_SERVER_DB_NAME=onlyoffice \ + -e MYSQL_SERVER_HOST=onlyoffice-mysql-server \ + -e MYSQL_SERVER_USER=onlyoffice_user \ + -e MYSQL_SERVER_PASS=onlyoffice_pass \ + + -e DOCUMENT_SERVER_PORT_80_TCP_ADDR=onlyoffice-document-server \ + -e DOCUMENT_SERVER_JWT_ENABLED=true \ + -e DOCUMENT_SERVER_JWT_SECRET=${JWT_SECRET} \ + -e DOCUMENT_SERVER_JWT_HEADER=AuthorizationJwt \ + + -e MAIL_SERVER_API_HOST=${MAIL_SERVER_IP} \ + -e MAIL_SERVER_DB_HOST=onlyoffice-mysql-server \ + -e MAIL_SERVER_DB_NAME=onlyoffice_mailserver \ + -e MAIL_SERVER_DB_PORT=3306 \ + -e MAIL_SERVER_DB_USER=root \ + -e MAIL_SERVER_DB_PASS=my-secret-pw \ + + -v /app/onlyoffice/CommunityServer/data:/var/www/onlyoffice/Data \ + -v /app/onlyoffice/CommunityServer/logs:/var/log/onlyoffice \ + -v /app/onlyoffice/CommunityServer/letsencrypt:/etc/letsencrypt \ + -v /sys/fs/cgroup:/sys/fs/cgroup:rw \ + onlyoffice/communityserver +``` + +Where `${MAIL_SERVER_IP}` is the IP address for **ONLYOFFICE Mail Server**. You can easily get it using the command: +``` +MAIL_SERVER_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' onlyoffice-mail-server) +``` + +Alternatively, you can use an automatic installation script to install the whole ONLYOFFICE Community Edition at once. For the mail server correct work you need to specify its hostname 'yourdomain.com'. + +**STEP 1**: Download the Community Edition Docker script file + +```bash +wget https://download.onlyoffice.com/install/opensource-install.sh +``` + +**STEP 2**: Install ONLYOFFICE Community Edition executing the following command: + +```bash +bash opensource-install.sh -md yourdomain.com +``` + +Or, use [docker-compose](https://docs.docker.com/compose/install "docker-compose"). For the mail server correct work you need to specify its hostname 'yourdomain.com'. Assuming you have docker-compose installed, execute the following command: + +```bash +wget https://raw.githubusercontent.com/ONLYOFFICE/Docker-CommunityServer/master/docker-compose.groups.yml +docker-compose up -d +``` + +## ONLYOFFICE Document Server ipv6 setup + +(Works and is supported only for Linux hosts) + +Docker does not currently provide ipv6 addresses to containers by default. This function is experimental now. + +To set up interaction via ipv6, you need to enable support for this feature in your Docker. For this you need: +- create the `/etc/docker/daemon.json` file with the following content: + +``` +{ +"ipv6": true, +"fixed-cidr-v6": "2001:db8:abc1::/64" +} +``` +- restart docker with the following command: `systemctl restart docker` + +After that, all running containers receive an ipv6 address and have an inet6 interface. + +You can check your default bridge network and see the field there +`EnableIPv6=true`. A new ipv6 subnet will also be added. + +For more information, visit the official [Docker manual site](https://docs.docker.com/config/daemon/ipv6/) + +## Issues + +### Docker Issues + +As a relatively new project Docker is being worked on and actively developed by its community. So it's recommended to use the latest version of Docker, because the issues that you encounter might have already been fixed with a newer Docker release. + +The known Docker issue with ONLYOFFICE Document Server with rpm-based distributives is that sometimes the processes fail to start inside Docker container. Fedora and RHEL/CentOS users should try disabling selinux with setenforce 0. If it fixes the issue then you can either stick with SELinux disabled which is not recommended by RedHat, or switch to using Ubuntu. + +### Document Server usage issues + +Due to the operational characteristic, **Document Server** saves a document only after the document has been closed by all the users who edited it. To avoid data loss, you must forcefully disconnect the **Document Server** users when you need to stop **Document Server** in cases of the application update, server reboot etc. To do that, execute the following script on the server where **Document Server** is installed: + +``` +sudo docker exec documentserver-prepare4shutdown.sh +``` + +Please note, that both executing the script and disconnecting users may take a long time (up to 5 minutes). + +## Project Information + +Official website: [https://www.onlyoffice.com](https://www.onlyoffice.com/?utm_source=github&utm_medium=cpc&utm_campaign=GitHubDockerDS) + +Code repository: [https://github.com/ONLYOFFICE/DocumentServer](https://github.com/ONLYOFFICE/DocumentServer "https://github.com/ONLYOFFICE/DocumentServer") + +Docker Image: [https://github.com/ONLYOFFICE/Docker-DocumentServer](https://github.com/ONLYOFFICE/Docker-DocumentServer "https://github.com/ONLYOFFICE/Docker-DocumentServer") + +License: [GNU AGPL v3.0](https://help.onlyoffice.com/products/files/doceditor.aspx?fileid=4358397&doc=K0ZUdlVuQzQ0RFhhMzhZRVN4ZFIvaHlhUjN2eS9XMXpKR1M5WEppUk1Gcz0_IjQzNTgzOTci0 "GNU AGPL v3.0") + +Free version vs commercial builds comparison: https://github.com/ONLYOFFICE/DocumentServer#onlyoffice-document-server-editions + +SaaS version: [https://www.onlyoffice.com/cloud-office.aspx](https://www.onlyoffice.com/cloud-office.aspx?utm_source=github&utm_medium=cpc&utm_campaign=GitHubDockerDS) + +## User Feedback and Support + +If you have any problems with or questions about this image, please visit our official forum to find answers to your questions: [forum.onlyoffice.com][1] or you can ask and answer ONLYOFFICE development questions on [Stack Overflow][2]. + + [1]: https://forum.onlyoffice.com + [2]: https://stackoverflow.com/questions/tagged/onlyoffice diff --git a/onlyoffice/config/supervisor/ds/ds-converter.conf b/onlyoffice/config/supervisor/ds/ds-converter.conf new file mode 100644 index 0000000..21daf98 --- /dev/null +++ b/onlyoffice/config/supervisor/ds/ds-converter.conf @@ -0,0 +1,13 @@ +[program:converter] +command=/var/www/COMPANY_NAME/documentserver/server/FileConverter/converter +directory=/var/www/COMPANY_NAME/documentserver/server/FileConverter +user=ds +environment=NODE_ENV=production-linux,NODE_CONFIG_DIR=/etc/COMPANY_NAME/documentserver,NODE_DISABLE_COLORS=1,APPLICATION_NAME=COMPANY_NAME,LD_LIBRARY_PATH=/var/www/COMPANY_NAME/documentserver/server/FileConverter/bin +stdout_logfile=/var/log/COMPANY_NAME/documentserver/converter/out.log +stdout_logfile_backups=0 +stdout_logfile_maxbytes=0 +stderr_logfile=/var/log/COMPANY_NAME/documentserver/converter/err.log +stderr_logfile_backups=0 +stderr_logfile_maxbytes=0 +autostart=true +autorestart=true diff --git a/onlyoffice/config/supervisor/ds/ds-docservice.conf b/onlyoffice/config/supervisor/ds/ds-docservice.conf new file mode 100644 index 0000000..7cb28f7 --- /dev/null +++ b/onlyoffice/config/supervisor/ds/ds-docservice.conf @@ -0,0 +1,13 @@ +[program:docservice] +command=/var/www/COMPANY_NAME/documentserver/server/DocService/docservice +directory=/var/www/COMPANY_NAME/documentserver/server/DocService +user=ds +environment=NODE_ENV=production-linux,NODE_CONFIG_DIR=/etc/COMPANY_NAME/documentserver,NODE_DISABLE_COLORS=1,APPLICATION_NAME=COMPANY_NAME +stdout_logfile=/var/log/COMPANY_NAME/documentserver/docservice/out.log +stdout_logfile_backups=0 +stdout_logfile_maxbytes=0 +stderr_logfile=/var/log/COMPANY_NAME/documentserver/docservice/err.log +stderr_logfile_backups=0 +stderr_logfile_maxbytes=0 +autostart=true +autorestart=true diff --git a/onlyoffice/config/supervisor/ds/ds-example.conf b/onlyoffice/config/supervisor/ds/ds-example.conf new file mode 100644 index 0000000..44fa688 --- /dev/null +++ b/onlyoffice/config/supervisor/ds/ds-example.conf @@ -0,0 +1,14 @@ +[program:example] +command=/var/www/COMPANY_NAME/documentserver-example/example +directory=/var/www/COMPANY_NAME/documentserver-example/ +user=ds +environment=NODE_ENV=production-linux,NODE_CONFIG_DIR=/etc/COMPANY_NAME/documentserver-example,NODE_DISABLE_COLORS=1 +stdout_logfile=/var/log/COMPANY_NAME/documentserver-example/out.log +stdout_logfile_backups=0 +stdout_logfile_maxbytes=0 +stderr_logfile=/var/log/COMPANY_NAME/documentserver-example/err.log +stderr_logfile_backups=0 +stderr_logfile_maxbytes=0 +autostart=false +autorestart=true +redirect_stderr=true diff --git a/onlyoffice/config/supervisor/ds/ds-metrics.conf b/onlyoffice/config/supervisor/ds/ds-metrics.conf new file mode 100644 index 0000000..d9c7576 --- /dev/null +++ b/onlyoffice/config/supervisor/ds/ds-metrics.conf @@ -0,0 +1,13 @@ +[program:metrics] +command=/var/www/COMPANY_NAME/documentserver/server/Metrics/metrics ./config/config.js +directory=/var/www/COMPANY_NAME/documentserver/server/Metrics +user=ds +environment=NODE_DISABLE_COLORS=1 +stdout_logfile=/var/log/COMPANY_NAME/documentserver/metrics/out.log +stdout_logfile_backups=0 +stdout_logfile_maxbytes=0 +stderr_logfile=/var/log/COMPANY_NAME/documentserver/metrics/err.log +stderr_logfile_backups=0 +stderr_logfile_maxbytes=0 +autostart=false +autorestart=false diff --git a/onlyoffice/config/supervisor/ds/ds.conf b/onlyoffice/config/supervisor/ds/ds.conf new file mode 100644 index 0000000..c9179df --- /dev/null +++ b/onlyoffice/config/supervisor/ds/ds.conf @@ -0,0 +1,2 @@ +[group:ds] +programs=docservice,converter,metrics,example diff --git a/onlyoffice/config/supervisor/supervisor b/onlyoffice/config/supervisor/supervisor new file mode 100644 index 0000000..1e612e0 --- /dev/null +++ b/onlyoffice/config/supervisor/supervisor @@ -0,0 +1,176 @@ +#! /bin/sh +# +# skeleton example file to build /etc/init.d/ scripts. +# This file should be used to construct scripts for /etc/init.d. +# +# Written by Miquel van Smoorenburg . +# Modified for Debian +# by Ian Murdock . +# Further changes by Javier Fernandez-Sanguino +# +# Version: @(#)skeleton 1.9 26-Feb-2001 miquels@cistron.nl +# +### BEGIN INIT INFO +# Provides: supervisor +# Required-Start: $remote_fs $network $named +# Required-Stop: $remote_fs $network $named +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Start/stop supervisor +# Description: Start/stop supervisor daemon and its configured +# subprocesses. +### END INIT INFO + +. /lib/lsb/init-functions + +PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin +DAEMON=/usr/bin/supervisord +NAME=supervisord +DESC=supervisor + +test -x $DAEMON || exit 0 + +LOGDIR=/var/log/supervisor +PIDFILE=/var/run/$NAME.pid +PS_COUNT=0 +DODTIME=5 # Time to wait for the server to die, in seconds + # If this value is set too low you might not + # let some servers to die gracefully and + # 'restart' will not work + +# Include supervisor defaults if available +if [ -f /etc/default/supervisor ] ; then + . /etc/default/supervisor +fi +DAEMON_OPTS="-c /etc/supervisor/supervisord.conf $DAEMON_OPTS" + +set -e + +running_pid() +{ + # Check if a given process pid's cmdline matches a given name + pid=$1 + name=$2 + [ -z "$pid" ] && return 1 + [ ! -d /proc/$pid ] && return 1 + (cat /proc/$pid/cmdline | tr "\000" "\n"|grep -q $name) || return 1 + return 0 +} + +running() +{ +# Check if the process is running looking at /proc +# (works for all users) + + # No pidfile, probably no daemon present + [ ! -f "$PIDFILE" ] && return 1 + # Obtain the pid and check it against the binary name + pid=`cat $PIDFILE` + running_pid $pid $DAEMON || return 1 + return 0 +} + +force_stop() { +# Forcefully kill the process + [ ! -f "$PIDFILE" ] && return + if running ; then + kill -15 $pid + # Is it really dead? + [ -n "$DODTIME" ] && sleep "$DODTIME"s + if running ; then + kill -9 $pid + [ -n "$DODTIME" ] && sleep "$DODTIME"s + if running ; then + echo "Cannot kill $LABEL (pid=$pid)!" + exit 1 + fi + fi + fi + rm -f $PIDFILE + return 0 +} + +get_pid() { + PS_COUNT=$(pgrep -fc $DAEMON || true) +} + +case "$1" in + start) + get_pid + if [ $PS_COUNT -eq 0 ]; then + rm -f "$PIDFILE" + fi + echo -n "Starting $DESC: " + start-stop-daemon --start --quiet --pidfile $PIDFILE \ + --startas $DAEMON -- $DAEMON_OPTS + test -f $PIDFILE || sleep 1 + if running ; then + echo "$NAME." + else + echo " ERROR." + fi + ;; + stop) + echo -n "Stopping $DESC: " + start-stop-daemon --stop --quiet --oknodo --pidfile $PIDFILE + echo "$NAME." + ;; + force-stop) + echo -n "Forcefully stopping $DESC: " + force_stop + if ! running ; then + echo "$NAME." + else + echo " ERROR." + fi + ;; + #reload) + # + # If the daemon can reload its config files on the fly + # for example by sending it SIGHUP, do it here. + # + # If the daemon responds to changes in its config file + # directly anyway, make this a do-nothing entry. + # + # echo "Reloading $DESC configuration files." + # start-stop-daemon --stop --signal 1 --quiet --pidfile \ + # /var/run/$NAME.pid --exec $DAEMON + #;; + force-reload) + # + # If the "reload" option is implemented, move the "force-reload" + # option to the "reload" entry above. If not, "force-reload" is + # just the same as "restart" except that it does nothing if the + # daemon isn't already running. + # check wether $DAEMON is running. If so, restart + start-stop-daemon --stop --test --quiet --pidfile $PIDFILE \ + --startas $DAEMON \ + && $0 restart \ + || exit 0 + ;; + restart) + echo -n "Restarting $DESC: " + start-stop-daemon --stop --quiet --oknodo --pidfile $PIDFILE + [ -n "$DODTIME" ] && sleep $DODTIME + start-stop-daemon --start --quiet --pidfile $PIDFILE \ + --startas $DAEMON -- $DAEMON_OPTS + echo "$NAME." + ;; + status) + echo -n "$LABEL is " + if running ; then + echo "running" + else + echo " not running." + exit 1 + fi + ;; + *) + N=/etc/init.d/$NAME + # echo "Usage: $N {start|stop|restart|reload|force-reload}" >&2 + echo "Usage: $N {start|stop|restart|force-reload|status|force-stop}" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/onlyoffice/create_infra.bash b/onlyoffice/create_infra.bash new file mode 100755 index 0000000..19c4006 --- /dev/null +++ b/onlyoffice/create_infra.bash @@ -0,0 +1,8 @@ +#!/bin/bash + +source .env +source ../core/.env + +docker exec postgres psql -U $POSTGRES_USER -d postgres -c "CREATE USER $ONLYOFFICE_DB_USER WITH PASSWORD '$ONLYOFFICE_DB_PASSWORD';" +docker exec postgres psql -U $POSTGRES_USER -d postgres -c "CREATE DATABASE $ONLYOFFICE_DB_NAME OWNER $ONLYOFFICE_DB_USER;" +docker exec postgres psql -U $POSTGRES_USER -d $ONLYOFFICE_DB_NAME -c "GRANT ALL PRIVILEGES ON DATABASE $ONLYOFFICE_DB_NAME TO $ONLYOFFICE_DB_USER;" diff --git a/onlyoffice/docker-bake.hcl b/onlyoffice/docker-bake.hcl new file mode 100644 index 0000000..7db339c --- /dev/null +++ b/onlyoffice/docker-bake.hcl @@ -0,0 +1,174 @@ +variable "TAG" { + default = "" +} + +variable "SHORTER_TAG" { + default = "" +} + +variable "SHORTEST_TAG" { + default = "" +} + +variable "PULL_TAG" { + default = "" +} + +variable "COMPANY_NAME" { + default = "" +} + +variable "PREFIX_NAME" { + default = "" +} + +variable "PRODUCT_EDITION" { + default = "" +} + +variable "PRODUCT_NAME" { + default = "" +} + +variable "PACKAGE_VERSION" { + default = "" +} + +variable "DOCKERFILE" { + default = "" +} + +variable "PLATFORM" { + default = "" +} + +variable "PACKAGE_BASEURL" { + default = "" +} + +variable "PACKAGE_FILE" { + default = "" +} + +variable "BUILD_CHANNEL" { + default = "" +} + +variable "PUSH_MAJOR" { + default = "false" +} + +variable "LATEST" { + default = "false" +} + +### ↓ Variables for UCS build ↓ + +variable "BASE_VERSION" { + default = "" +} + +variable "PACKAGE_SUFFIX" { + default = "" +} + +variable "PG_VERSION" { + default = "" +} + +variable "UCS_REBUILD" { + default = "" +} + +variable "UCS_PREFIX" { + default = "" +} + +### ↑ Variables for UCS build ↑ + +target "documentserver" { + target = "documentserver" + dockerfile = "${DOCKERFILE}" + tags = [ + "docker.io/${COMPANY_NAME}/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}:${TAG}", + equal("nightly",BUILD_CHANNEL) ? "docker.io/${COMPANY_NAME}/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}:latest": "", + ] + platforms = ["${PLATFORM}"] + args = { + "COMPANY_NAME": "${COMPANY_NAME}" + "PRODUCT_NAME": "${PRODUCT_NAME}" + "PRODUCT_EDITION": "${PRODUCT_EDITION}" + "PACKAGE_VERSION": "${PACKAGE_VERSION}" + "PACKAGE_BASEURL": "${PACKAGE_BASEURL}" + "PLATFORM": "${PLATFORM}" + } +} + +target "documentserver-stable" { + target = "documentserver-stable" + dockerfile = "production.dockerfile" + tags = ["docker.io/${COMPANY_NAME}/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}:${TAG}", + "docker.io/${COMPANY_NAME}/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}:${SHORTER_TAG}", + "docker.io/${COMPANY_NAME}/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}:${SHORTEST_TAG}", + "docker.io/${COMPANY_NAME}/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}:latest", + equal("-ee",PRODUCT_EDITION) ? "docker.io/${COMPANY_NAME}4enterprise/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}:${TAG}": "",] + platforms = ["linux/amd64", "linux/arm64"] + args = { + "PULL_TAG": "${PULL_TAG}" + "COMPANY_NAME": "${COMPANY_NAME}" + "PRODUCT_NAME": "${PRODUCT_NAME}" + "PRODUCT_EDITION": "${PRODUCT_EDITION}" + } +} + +target "documentserver-ucs" { + target = "documentserver" + dockerfile = "${DOCKERFILE}" + tags = [ + "docker.io/${COMPANY_NAME}/${PRODUCT_NAME}${PRODUCT_EDITION}-ucs:${TAG}" + ] + platforms = ["linux/amd64", "linux/arm64"] + args = { + "PRODUCT_EDITION": "${PRODUCT_EDITION}" + "PRODUCT_NAME": "${PRODUCT_NAME}" + "COMPANY_NAME": "${COMPANY_NAME}" + "PACKAGE_VERSION": "${PACKAGE_VERSION}" + "PACKAGE_BASEURL": "${PACKAGE_BASEURL}" + "PACKAGE_SUFFIX": "${PACKAGE_SUFFIX}" + "BASE_VERSION": "${BASE_VERSION}" + "PG_VERSION": "${PG_VERSION}" + } +} + +target "documentserver-nonexample" { + target = "documentserver-nonexample" + dockerfile = "production.dockerfile" + tags = [ "docker.io/${COMPANY_NAME}/${PRODUCT_NAME}${PREFIX_NAME}${PRODUCT_EDITION}:${TAG}-nonexample" ] + platforms = ["linux/amd64", "linux/arm64"] + args = { + "PULL_TAG": "${PULL_TAG}" + "COMPANY_NAME": "${COMPANY_NAME}" + "PRODUCT_NAME": "${PRODUCT_NAME}" + "PRODUCT_EDITION": "${PRODUCT_EDITION}" + } +} + +target "documentserver-stable-rebuild" { + target = "documentserver-stable-rebuild" + dockerfile = "production.dockerfile" + tags = equal("true",UCS_REBUILD) ? ["docker.io/${COMPANY_NAME}/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}-ucs:${TAG}",] : [ + "docker.io/${COMPANY_NAME}/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}:${TAG}", + equal("",PREFIX_NAME) ? "docker.io/${COMPANY_NAME}/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}:${SHORTER_TAG}": "", + equal("true",PUSH_MAJOR) ? "docker.io/${COMPANY_NAME}/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}:${SHORTEST_TAG}": "", + equal("",PREFIX_NAME) && equal("true",LATEST) ? "docker.io/${COMPANY_NAME}/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}:latest": "", + equal("-ee",PRODUCT_EDITION) && equal("",PREFIX_NAME) ? "docker.io/${COMPANY_NAME}4enterprise/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}:${TAG}": "", + ] + platforms = ["linux/amd64", "linux/arm64"] + args = { + "UCS_PREFIX": "${UCS_PREFIX}" + "PULL_TAG": "${PULL_TAG}" + "COMPANY_NAME": "${COMPANY_NAME}" + "PRODUCT_NAME": "${PRODUCT_NAME}" + "PRODUCT_EDITION": "${PRODUCT_EDITION}" + } +} diff --git a/onlyoffice/docker-compose.yml b/onlyoffice/docker-compose.yml new file mode 100644 index 0000000..daaeb20 --- /dev/null +++ b/onlyoffice/docker-compose.yml @@ -0,0 +1,50 @@ +services: + onlyoffice-document-server: + build: + context: . + container_name: onlyoffice-document-server + depends_on: + - onlyoffice-rabbitmq + environment: + - DB_TYPE=${ONLYOFFICE_DB_TYPE} + - DB_HOST=${ONLYOFFICE_DB_HOST} + - DB_PORT=${ONLYOFFICE_DB_PORT} + - DB_NAME=${ONLYOFFICE_DB_NAME} + - DB_USER=${ONLYOFFICE_DB_USER} + - DB_PWD=${ONLYOFFICE_DB_PASSWORD} + - AMQP_URI=amqp://guest:guest@onlyoffice-rabbitmq + # Uncomment strings below to enable the JSON Web Token validation. + - JWT_ENABLED=true + - JWT_SECRET=${ONLYOFFICE_JWT_SECRET} + - JWT_HEADER=Authorization + - JWT_IN_BODY=true + stdin_open: true + stop_grace_period: 60s + volumes: + - /var/www/onlyoffice/Data + - /var/log/onlyoffice + - /var/lib/onlyoffice/documentserver/App_Data/cache/files + - /var/www/onlyoffice/documentserver-example/public/files + - /usr/share/fonts + networks: + - database + - nextcloud + - onlyoffice + restart: unless-stopped + + onlyoffice-rabbitmq: + container_name: onlyoffice-rabbitmq + image: rabbitmq + restart: unless-stopped + expose: + - '5672' + networks: + - onlyoffice + +networks: + database: + external: true + nextcloud: + external: true + onlyoffice: + name: onlyoffice diff --git a/onlyoffice/oracle/sqlplus b/onlyoffice/oracle/sqlplus new file mode 100755 index 0000000..3bd1bd5 --- /dev/null +++ b/onlyoffice/oracle/sqlplus @@ -0,0 +1,6 @@ +#!/bin/sh + +CLIENTDIR=/usr/share/instantclient +export LD_LIBRARY_PATH=$CLIENTDIR +$CLIENTDIR/sqlplus $@ + diff --git a/onlyoffice/production.dockerfile b/onlyoffice/production.dockerfile new file mode 100644 index 0000000..24c6569 --- /dev/null +++ b/onlyoffice/production.dockerfile @@ -0,0 +1,33 @@ +### Arguments avavlivable only for FROM instruction ### +ARG PULL_TAG=latest +ARG COMPANY_NAME=onlyoffice +ARG PRODUCT_EDITION= +### Rebuild arguments +ARG UCS_PREFIX= +ARG IMAGE=${COMPANY_NAME}/documentserver${PRODUCT_EDITION}${UCS_PREFIX}:${PULL_TAG} + +### Build main-release ### + +FROM ${COMPANY_NAME}/4testing-documentserver${PRODUCT_EDITION}:${PULL_TAG} as documentserver-stable + +### Rebuild stable images with secure updates +FROM ${IMAGE} as documentserver-stable-rebuild +RUN echo "This is rebuild" \ + && apt-get update -y \ + && apt-get upgrade -y + +### Build nonexample ### + +FROM ${COMPANY_NAME}/documentserver${PRODUCT_EDITION}:${PULL_TAG} as documentserver-nonexample + +ARG COMPANY_NAME=onlyoffice +ARG PRODUCT_NAME=documentserver +ARG DS_SUPERVISOR_CONF=/etc/supervisor/conf.d/ds.conf + +### Remove all documentserver-example data ### + +RUN rm -rf /var/www/$COMPANY_NAME/$PRODUCT_NAME-example \ + && rm -rf /etc/$COMPANY_NAME/$PRODUCT_NAME-example \ + && rm -f $DS_SUPERVISOR_CONF \ + && rm -f /etc/nginx/includes/ds-example.conf \ + && ln -s /etc/$COMPANY_NAME/$PRODUCT_NAME/supervisor/ds.conf $DS_SUPERVISOR_CONF diff --git a/onlyoffice/run-document-server.sh b/onlyoffice/run-document-server.sh new file mode 100644 index 0000000..f751b3c --- /dev/null +++ b/onlyoffice/run-document-server.sh @@ -0,0 +1,795 @@ +#!/bin/bash + +umask 0022 + +start_process() { + "$@" & + CHILD=$!; wait "$CHILD"; CHILD=""; +} + +function clean_exit { + [[ -z "$CHILD" ]] || kill -s SIGTERM "$CHILD" 2>/dev/null + if [ ${ONLYOFFICE_DATA_CONTAINER} == "false" ] && \ + [ ${ONLYOFFICE_DATA_CONTAINER_HOST} == "localhost" ]; then + /usr/bin/documentserver-prepare4shutdown.sh + fi + exit +} + +trap clean_exit SIGTERM SIGQUIT SIGABRT SIGINT + +# Define '**' behavior explicitly +shopt -s globstar + +APP_DIR="/var/www/${COMPANY_NAME}/documentserver" +DATA_DIR="/var/www/${COMPANY_NAME}/Data" +PRIVATE_DATA_DIR="${DATA_DIR}/.private" +DS_RELEASE_DATE="${PRIVATE_DATA_DIR}/ds_release_date" +LOG_DIR="/var/log/${COMPANY_NAME}" +DS_LOG_DIR="${LOG_DIR}/documentserver" +LIB_DIR="/var/lib/${COMPANY_NAME}" +DS_LIB_DIR="${LIB_DIR}/documentserver" +CONF_DIR="/etc/${COMPANY_NAME}/documentserver" +SUPERVISOR_CONF_DIR="/etc/supervisor/conf.d" +IS_UPGRADE="false" +PLUGINS_ENABLED=${PLUGINS_ENABLED:-true} + +ONLYOFFICE_DATA_CONTAINER=${ONLYOFFICE_DATA_CONTAINER:-false} +ONLYOFFICE_DATA_CONTAINER_HOST=${ONLYOFFICE_DATA_CONTAINER_HOST:-localhost} +ONLYOFFICE_DATA_CONTAINER_PORT=80 + +RELEASE_DATE="$(stat -c="%y" ${APP_DIR}/server/DocService/docservice | sed -r 's/=([0-9]+)-([0-9]+)-([0-9]+) ([0-9:.+ ]+)/\1-\2-\3/')"; +if [ -f ${DS_RELEASE_DATE} ]; then + PREV_RELEASE_DATE=$(head -n 1 ${DS_RELEASE_DATE}) +else + PREV_RELEASE_DATE="0" +fi + +if [ "${RELEASE_DATE}" != "${PREV_RELEASE_DATE}" ]; then + if [ ${ONLYOFFICE_DATA_CONTAINER} != "true" ]; then + IS_UPGRADE="true"; + fi +fi + +SSL_CERTIFICATES_DIR="/usr/share/ca-certificates/ds" +mkdir -p ${SSL_CERTIFICATES_DIR} +if find "${DATA_DIR}/certs" -type f \( -name "*.crt" -o -name "*.pem" \) -print -quit >/dev/null 2>&1; then + cp -f ${DATA_DIR}/certs/* ${SSL_CERTIFICATES_DIR} + chmod 644 ${SSL_CERTIFICATES_DIR}/*.{crt,pem} 2>/dev/null + chmod 400 ${SSL_CERTIFICATES_DIR}/*.key 2>/dev/null +fi + +if [[ -z $SSL_CERTIFICATE_PATH ]] && [[ -f ${SSL_CERTIFICATES_DIR}/${COMPANY_NAME}.crt ]]; then + SSL_CERTIFICATE_PATH=${SSL_CERTIFICATES_DIR}/${COMPANY_NAME}.crt +else + SSL_CERTIFICATE_PATH=${SSL_CERTIFICATE_PATH:-${SSL_CERTIFICATES_DIR}/tls.crt} +fi +if [[ -z $SSL_KEY_PATH ]] && [[ -f ${SSL_CERTIFICATES_DIR}/${COMPANY_NAME}.key ]]; then + SSL_KEY_PATH=${SSL_CERTIFICATES_DIR}/${COMPANY_NAME}.key +else + SSL_KEY_PATH=${SSL_KEY_PATH:-${SSL_CERTIFICATES_DIR}/tls.key} +fi + +#When set, the well known "root" CAs will be extended with the extra certificates in file +NODE_EXTRA_CA_CERTS=${NODE_EXTRA_CA_CERTS:-${SSL_CERTIFICATES_DIR}/extra-ca-certs.pem} +if [[ -f ${NODE_EXTRA_CA_CERTS} ]]; then + NODE_EXTRA_ENVIRONMENT="${NODE_EXTRA_CA_CERTS}" +elif [[ -f ${SSL_CERTIFICATE_PATH} ]]; then + SSL_CERTIFICATE_SUBJECT=$(openssl x509 -subject -noout -in "${SSL_CERTIFICATE_PATH}" | sed 's/subject=//') + SSL_CERTIFICATE_ISSUER=$(openssl x509 -issuer -noout -in "${SSL_CERTIFICATE_PATH}" | sed 's/issuer=//') + + #Add self-signed certificate to trusted list for validating Docs requests to the test example + if [[ -n $SSL_CERTIFICATE_SUBJECT && $SSL_CERTIFICATE_SUBJECT == $SSL_CERTIFICATE_ISSUER ]]; then + NODE_EXTRA_ENVIRONMENT="${SSL_CERTIFICATE_PATH}" + fi +fi + +if [[ -n $NODE_EXTRA_ENVIRONMENT ]]; then + sed -i "s|^environment=.*$|&,NODE_EXTRA_CA_CERTS=${NODE_EXTRA_ENVIRONMENT}|" /etc/supervisor/conf.d/*.conf +fi + +CA_CERTIFICATES_PATH=${CA_CERTIFICATES_PATH:-${SSL_CERTIFICATES_DIR}/ca-certificates.pem} +SSL_DHPARAM_PATH=${SSL_DHPARAM_PATH:-${SSL_CERTIFICATES_DIR}/dhparam.pem} +SSL_VERIFY_CLIENT=${SSL_VERIFY_CLIENT:-off} +USE_UNAUTHORIZED_STORAGE=${USE_UNAUTHORIZED_STORAGE:-false} +ONLYOFFICE_HTTPS_HSTS_ENABLED=${ONLYOFFICE_HTTPS_HSTS_ENABLED:-true} +ONLYOFFICE_HTTPS_HSTS_MAXAGE=${ONLYOFFICE_HTTPS_HSTS_MAXAGE:-31536000} +SYSCONF_TEMPLATES_DIR="/app/ds/setup/config" + +NGINX_CONFD_PATH="/etc/nginx/conf.d"; +NGINX_ONLYOFFICE_PATH="${CONF_DIR}/nginx" +NGINX_ONLYOFFICE_CONF="${NGINX_ONLYOFFICE_PATH}/ds.conf" +NGINX_ONLYOFFICE_EXAMPLE_PATH="${CONF_DIR}-example/nginx" +NGINX_ONLYOFFICE_EXAMPLE_CONF="${NGINX_ONLYOFFICE_EXAMPLE_PATH}/includes/ds-example.conf" + +NGINX_CONFIG_PATH="/etc/nginx/nginx.conf" +NGINX_WORKER_PROCESSES=${NGINX_WORKER_PROCESSES:-1} +# Limiting the maximum number of simultaneous connections due to possible memory shortage +LIMIT=$(ulimit -n); [ $LIMIT -gt 1048576 ] && LIMIT=1048576 +NGINX_WORKER_CONNECTIONS=${NGINX_WORKER_CONNECTIONS:-$LIMIT} +RABBIT_CONNECTIONS=${RABBIT_CONNECTIONS:-$LIMIT} + +JWT_ENABLED=${JWT_ENABLED:-true} + +# validate user's vars before usinig in json +if [ "${JWT_ENABLED}" == "true" ]; then + JWT_ENABLED="true" +else + JWT_ENABLED="false" +fi + +[ -z $JWT_SECRET ] && JWT_MESSAGE='JWT is enabled by default. A random secret is generated automatically. Run the command "docker exec $(sudo docker ps -q) sudo documentserver-jwt-status.sh" to get information about JWT.' + +JWT_SECRET=${JWT_SECRET:-$(pwgen -s 32)} +JWT_HEADER=${JWT_HEADER:-Authorization} +JWT_IN_BODY=${JWT_IN_BODY:-false} + +WOPI_ENABLED=${WOPI_ENABLED:-false} +ALLOW_META_IP_ADDRESS=${ALLOW_META_IP_ADDRESS:-false} +ALLOW_PRIVATE_IP_ADDRESS=${ALLOW_PRIVATE_IP_ADDRESS:-false} + +GENERATE_FONTS=${GENERATE_FONTS:-true} + +if [[ ${PRODUCT_NAME}${PRODUCT_EDITION} == "documentserver" ]]; then + REDIS_ENABLED=false +else + REDIS_ENABLED=true +fi + +ONLYOFFICE_DEFAULT_CONFIG=${CONF_DIR}/local.json +ONLYOFFICE_LOG4JS_CONFIG=${CONF_DIR}/log4js/production.json +ONLYOFFICE_EXAMPLE_CONFIG=${CONF_DIR}-example/local.json + +JSON_BIN=${APP_DIR}/npm/json +JSON="${JSON_BIN} -q -f ${ONLYOFFICE_DEFAULT_CONFIG}" +JSON_LOG="${JSON_BIN} -q -f ${ONLYOFFICE_LOG4JS_CONFIG}" +JSON_EXAMPLE="${JSON_BIN} -q -f ${ONLYOFFICE_EXAMPLE_CONFIG}" + +LOCAL_SERVICES=() + +PG_ROOT=/var/lib/postgresql +PG_NAME=main +PGDATA=${PG_ROOT}/${PG_VERSION}/${PG_NAME} +PG_NEW_CLUSTER=false +RABBITMQ_DATA=/var/lib/rabbitmq +REDIS_DATA=/var/lib/redis + +if [ "${LETS_ENCRYPT_DOMAIN}" != "" -a "${LETS_ENCRYPT_MAIL}" != "" ]; then + LETSENCRYPT_ROOT_DIR="/etc/letsencrypt/live" + SSL_CERTIFICATE_PATH=${LETSENCRYPT_ROOT_DIR}/${LETS_ENCRYPT_DOMAIN}/fullchain.pem + SSL_KEY_PATH=${LETSENCRYPT_ROOT_DIR}/${LETS_ENCRYPT_DOMAIN}/privkey.pem +fi + +read_setting(){ + deprecated_var POSTGRESQL_SERVER_HOST DB_HOST + deprecated_var POSTGRESQL_SERVER_PORT DB_PORT + deprecated_var POSTGRESQL_SERVER_DB_NAME DB_NAME + deprecated_var POSTGRESQL_SERVER_USER DB_USER + deprecated_var POSTGRESQL_SERVER_PASS DB_PWD + deprecated_var RABBITMQ_SERVER_URL AMQP_URI + deprecated_var AMQP_SERVER_URL AMQP_URI + deprecated_var AMQP_SERVER_TYPE AMQP_TYPE + + METRICS_ENABLED="${METRICS_ENABLED:-false}" + METRICS_HOST="${METRICS_HOST:-localhost}" + METRICS_PORT="${METRICS_PORT:-8125}" + METRICS_PREFIX="${METRICS_PREFIX:-.ds}" + + DB_HOST=${DB_HOST:-${POSTGRESQL_SERVER_HOST:-$(${JSON} services.CoAuthoring.sql.dbHost)}} + DB_TYPE=${DB_TYPE:-$(${JSON} services.CoAuthoring.sql.type)} + case $DB_TYPE in + "postgres") + DB_PORT=${DB_PORT:-"5432"} + ;; + "mariadb"|"mysql") + DB_PORT=${DB_PORT:-"3306"} + ;; + "dameng") + DB_PORT=${DB_PORT:-"5236"} + ;; + "mssql") + DB_PORT=${DB_PORT:-"1433"} + ;; + "oracle") + DB_PORT=${DB_PORT:-"1521"} + ;; + "") + DB_PORT=${DB_PORT:-${POSTGRESQL_SERVER_PORT:-$(${JSON} services.CoAuthoring.sql.dbPort)}} + ;; + *) + echo "ERROR: unknown database type" + exit 1 + ;; + esac + DB_NAME=${DB_NAME:-${POSTGRESQL_SERVER_DB_NAME:-$(${JSON} services.CoAuthoring.sql.dbName)}} + DB_USER=${DB_USER:-${POSTGRESQL_SERVER_USER:-$(${JSON} services.CoAuthoring.sql.dbUser)}} + DB_PWD=${DB_PWD:-${POSTGRESQL_SERVER_PASS:-$(${JSON} services.CoAuthoring.sql.dbPass)}} + + RABBITMQ_SERVER_URL=${RABBITMQ_SERVER_URL:-$(${JSON} rabbitmq.url)} + AMQP_URI=${AMQP_URI:-${AMQP_SERVER_URL:-${RABBITMQ_SERVER_URL}}} + AMQP_TYPE=${AMQP_TYPE:-${AMQP_SERVER_TYPE:-rabbitmq}} + parse_rabbitmq_url ${AMQP_URI} + + REDIS_SERVER_HOST=${REDIS_SERVER_HOST:-$(${JSON} services.CoAuthoring.redis.host)} + REDIS_SERVER_PORT=${REDIS_SERVER_PORT:-6379} + + DS_LOG_LEVEL=${DS_LOG_LEVEL:-$(${JSON_LOG} categories.default.level)} +} + +deprecated_var() { + if [[ -n ${!1} ]]; then + echo "Variable $1 is deprecated. Use $2 instead." + fi +} + +parse_rabbitmq_url(){ + local amqp=$1 + + # extract the protocol + local proto="$(echo $amqp | grep :// | sed -e's,^\(.*://\).*,\1,g')" + # remove the protocol + local url="$(echo ${amqp/$proto/})" + + # extract the user and password (if any) + local userpass="`echo $url | grep @ | cut -d@ -f1`" + local pass=`echo $userpass | grep : | cut -d: -f2` + + local user + if [ -n "$pass" ]; then + user=`echo $userpass | grep : | cut -d: -f1` + else + user=$userpass + fi + + # extract the host + local hostport="$(echo ${url/$userpass@/} | cut -d/ -f1)" + # by request - try to extract the port + local port="$(echo $hostport | grep : | sed -r 's_^.*:+|/.*$__g')" + + local host + if [ -n "$port" ]; then + host=`echo $hostport | grep : | cut -d: -f1` + else + host=$hostport + port="5672" + fi + + # extract the path (if any) + local path="$(echo $url | grep / | cut -d/ -f2-)" + + AMQP_SERVER_PROTO=${proto:0:-3} + AMQP_SERVER_HOST=$host + AMQP_SERVER_USER=$user + AMQP_SERVER_PASS=$pass + AMQP_SERVER_PORT=$port +} + +waiting_for_connection(){ + until nc -z -w 3 "$1" "$2"; do + >&2 echo "Waiting for connection to the $1 host on port $2" + sleep 1 + done +} + +waiting_for_db_ready(){ + case $DB_TYPE in + "oracle") + PDB="XEPDB1" + ORACLE_SQL="sqlplus $DB_USER/$DB_PWD@//$DB_HOST:$DB_PORT/$PDB" + DB_TEST="echo \"SELECT version FROM V\$INSTANCE;\" | $ORACLE_SQL 2>/dev/null | grep \"Connected\" | wc -l" + ;; + *) + return + ;; + esac + + for (( i=1; i <= 10; i++ )); do + RES=$(eval $DB_TEST) + if [ "$RES" -ne "0" ]; then + echo "Database is ready" + break + fi + sleep 5 + done +} + +waiting_for_db(){ + waiting_for_connection $DB_HOST $DB_PORT + waiting_for_db_ready +} + +waiting_for_amqp(){ + waiting_for_connection ${AMQP_SERVER_HOST} ${AMQP_SERVER_PORT} +} + +waiting_for_redis(){ + waiting_for_connection ${REDIS_SERVER_HOST} ${REDIS_SERVER_PORT} +} +waiting_for_datacontainer(){ + waiting_for_connection ${ONLYOFFICE_DATA_CONTAINER_HOST} ${ONLYOFFICE_DATA_CONTAINER_PORT} +} + +update_statsd_settings(){ + ${JSON} -I -e "if(this.statsd===undefined)this.statsd={};" + ${JSON} -I -e "this.statsd.useMetrics = '${METRICS_ENABLED}'" + ${JSON} -I -e "this.statsd.host = '${METRICS_HOST}'" + ${JSON} -I -e "this.statsd.port = '${METRICS_PORT}'" + ${JSON} -I -e "this.statsd.prefix = '${METRICS_PREFIX}'" + sed -i -E "s/(autostart|autorestart)=.*$/\1=${METRICS_ENABLED}/g" ${SUPERVISOR_CONF_DIR}/ds-metrics.conf +} + +update_db_settings(){ + ${JSON} -I -e "this.services.CoAuthoring.sql.type = '${DB_TYPE}'" + ${JSON} -I -e "this.services.CoAuthoring.sql.dbHost = '${DB_HOST}'" + ${JSON} -I -e "this.services.CoAuthoring.sql.dbPort = '${DB_PORT}'" + ${JSON} -I -e "this.services.CoAuthoring.sql.dbName = '${DB_NAME}'" + ${JSON} -I -e "this.services.CoAuthoring.sql.dbUser = '${DB_USER}'" + ${JSON} -I -e "this.services.CoAuthoring.sql.dbPass = '${DB_PWD}'" +} + +update_rabbitmq_setting(){ + if [ "${AMQP_TYPE}" == "rabbitmq" ]; then + ${JSON} -I -e "if(this.queue===undefined)this.queue={};" + ${JSON} -I -e "this.queue.type = 'rabbitmq'" + ${JSON} -I -e "this.rabbitmq.url = '${AMQP_URI}'" + fi + + if [ "${AMQP_TYPE}" == "activemq" ]; then + ${JSON} -I -e "if(this.queue===undefined)this.queue={};" + ${JSON} -I -e "this.queue.type = 'activemq'" + ${JSON} -I -e "if(this.activemq===undefined)this.activemq={};" + ${JSON} -I -e "if(this.activemq.connectOptions===undefined)this.activemq.connectOptions={};" + + ${JSON} -I -e "this.activemq.connectOptions.host = '${AMQP_SERVER_HOST}'" + + if [ ! "${AMQP_SERVER_PORT}" == "" ]; then + ${JSON} -I -e "this.activemq.connectOptions.port = '${AMQP_SERVER_PORT}'" + else + ${JSON} -I -e "delete this.activemq.connectOptions.port" + fi + + if [ ! "${AMQP_SERVER_USER}" == "" ]; then + ${JSON} -I -e "this.activemq.connectOptions.username = '${AMQP_SERVER_USER}'" + else + ${JSON} -I -e "delete this.activemq.connectOptions.username" + fi + + if [ ! "${AMQP_SERVER_PASS}" == "" ]; then + ${JSON} -I -e "this.activemq.connectOptions.password = '${AMQP_SERVER_PASS}'" + else + ${JSON} -I -e "delete this.activemq.connectOptions.password" + fi + + case "${AMQP_SERVER_PROTO}" in + amqp+ssl|amqps) + ${JSON} -I -e "this.activemq.connectOptions.transport = 'tls'" + ;; + *) + ${JSON} -I -e "delete this.activemq.connectOptions.transport" + ;; + esac + fi +} + +update_redis_settings(){ + ${JSON} -I -e "if(this.services.CoAuthoring.redis===undefined)this.services.CoAuthoring.redis={};" + ${JSON} -I -e "this.services.CoAuthoring.redis.host = '${REDIS_SERVER_HOST}'" + ${JSON} -I -e "this.services.CoAuthoring.redis.port = '${REDIS_SERVER_PORT}'" + + if [ -n "${REDIS_SERVER_PASS}" ]; then + ${JSON} -I -e "this.services.CoAuthoring.redis.options = {'password':'${REDIS_SERVER_PASS}'}" + fi + +} + +update_ds_settings(){ + ${JSON} -I -e "this.services.CoAuthoring.token.enable.browser = ${JWT_ENABLED}" + ${JSON} -I -e "this.services.CoAuthoring.token.enable.request.inbox = ${JWT_ENABLED}" + ${JSON} -I -e "this.services.CoAuthoring.token.enable.request.outbox = ${JWT_ENABLED}" + + ${JSON} -I -e "this.services.CoAuthoring.secret.inbox.string = '${JWT_SECRET}'" + ${JSON} -I -e "this.services.CoAuthoring.secret.outbox.string = '${JWT_SECRET}'" + ${JSON} -I -e "this.services.CoAuthoring.secret.session.string = '${JWT_SECRET}'" + + ${JSON} -I -e "this.services.CoAuthoring.token.inbox.header = '${JWT_HEADER}'" + ${JSON} -I -e "this.services.CoAuthoring.token.outbox.header = '${JWT_HEADER}'" + + ${JSON} -I -e "this.services.CoAuthoring.token.inbox.inBody = ${JWT_IN_BODY}" + ${JSON} -I -e "this.services.CoAuthoring.token.outbox.inBody = ${JWT_IN_BODY}" + + if [ -f "${ONLYOFFICE_EXAMPLE_CONFIG}" ]; then + ${JSON_EXAMPLE} -I -e "this.server.token.enable = ${JWT_ENABLED}" + ${JSON_EXAMPLE} -I -e "this.server.token.secret = '${JWT_SECRET}'" + ${JSON_EXAMPLE} -I -e "this.server.token.authorizationHeader = '${JWT_HEADER}'" + fi + + if [ "${USE_UNAUTHORIZED_STORAGE}" == "true" ]; then + ${JSON} -I -e "if(this.services.CoAuthoring.requestDefaults===undefined)this.services.CoAuthoring.requestDefaults={}" + ${JSON} -I -e "if(this.services.CoAuthoring.requestDefaults.rejectUnauthorized===undefined)this.services.CoAuthoring.requestDefaults.rejectUnauthorized=false" + fi + + WOPI_PRIVATE_KEY="${DATA_DIR}/wopi_private.key" + WOPI_PUBLIC_KEY="${DATA_DIR}/wopi_public.key" + + [ ! -f "${WOPI_PRIVATE_KEY}" ] && echo -n "Generating WOPI private key..." && openssl genpkey -algorithm RSA -outform PEM -out "${WOPI_PRIVATE_KEY}" >/dev/null 2>&1 && echo "Done" + [ ! -f "${WOPI_PUBLIC_KEY}" ] && echo -n "Generating WOPI public key..." && openssl rsa -RSAPublicKey_out -in "${WOPI_PRIVATE_KEY}" -outform "MS PUBLICKEYBLOB" -out "${WOPI_PUBLIC_KEY}" >/dev/null 2>&1 && echo "Done" + WOPI_MODULUS=$(openssl rsa -pubin -inform "MS PUBLICKEYBLOB" -modulus -noout -in "${WOPI_PUBLIC_KEY}" | sed 's/Modulus=//' | xxd -r -p | openssl base64 -A) + WOPI_EXPONENT=$(openssl rsa -pubin -inform "MS PUBLICKEYBLOB" -text -noout -in "${WOPI_PUBLIC_KEY}" | grep -oP '(?<=Exponent: )\d+') + + ${JSON} -I -e "if(this.wopi===undefined)this.wopi={};" + ${JSON} -I -e "this.wopi.enable = ${WOPI_ENABLED}" + ${JSON} -I -e "this.wopi.privateKey = '$(awk '{printf "%s\\n", $0}' ${WOPI_PRIVATE_KEY})'" + ${JSON} -I -e "this.wopi.privateKeyOld = '$(awk '{printf "%s\\n", $0}' ${WOPI_PRIVATE_KEY})'" + ${JSON} -I -e "this.wopi.publicKey = '$(openssl base64 -in ${WOPI_PUBLIC_KEY} -A)'" + ${JSON} -I -e "this.wopi.publicKeyOld = '$(openssl base64 -in ${WOPI_PUBLIC_KEY} -A)'" + ${JSON} -I -e "this.wopi.modulus = '${WOPI_MODULUS}'" + ${JSON} -I -e "this.wopi.modulusOld = '${WOPI_MODULUS}'" + ${JSON} -I -e "this.wopi.exponent = ${WOPI_EXPONENT}" + ${JSON} -I -e "this.wopi.exponentOld = ${WOPI_EXPONENT}" + + if [ "${ALLOW_META_IP_ADDRESS}" = "true" ] || [ "${ALLOW_PRIVATE_IP_ADDRESS}" = "true" ]; then + ${JSON} -I -e "if(this.services.CoAuthoring['request-filtering-agent']===undefined)this.services.CoAuthoring['request-filtering-agent']={}" + [ "${ALLOW_META_IP_ADDRESS}" = "true" ] && ${JSON} -I -e "this.services.CoAuthoring['request-filtering-agent'].allowMetaIPAddress = true" + [ "${ALLOW_PRIVATE_IP_ADDRESS}" = "true" ] && ${JSON} -I -e "this.services.CoAuthoring['request-filtering-agent'].allowPrivateIPAddress = true" + fi +} + +create_postgresql_cluster(){ + local pg_conf_dir=/etc/postgresql/${PG_VERSION}/${PG_NAME} + local postgresql_conf=$pg_conf_dir/postgresql.conf + local hba_conf=$pg_conf_dir/pg_hba.conf + + mv $postgresql_conf $postgresql_conf.backup + mv $hba_conf $hba_conf.backup + + pg_createcluster ${PG_VERSION} ${PG_NAME} +} + +create_postgresql_db(){ + sudo -u postgres psql -c "CREATE USER $DB_USER WITH password '"$DB_PWD"';" + sudo -u postgres psql -c "CREATE DATABASE $DB_NAME OWNER $DB_USER;" +} + +create_mssql_db(){ + MSSQL="/opt/mssql-tools18/bin/sqlcmd -S $DB_HOST,$DB_PORT" + + $MSSQL -U $DB_USER -P "$DB_PWD" -C -Q "IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = '$DB_NAME') BEGIN CREATE DATABASE $DB_NAME; END" +} + +create_db_tbl() { + case $DB_TYPE in + "postgres") + create_postgresql_tbl + ;; + "mariadb"|"mysql") + create_mysql_tbl + ;; + "mssql") + create_mssql_tbl + ;; + "oracle") + create_oracle_tbl + ;; + esac +} + +upgrade_db_tbl() { + case $DB_TYPE in + "postgres") + upgrade_postgresql_tbl + ;; + "mariadb"|"mysql") + upgrade_mysql_tbl + ;; + "mssql") + upgrade_mssql_tbl + ;; + "oracle") + upgrade_oracle_tbl + ;; + esac +} + +upgrade_postgresql_tbl() { + if [ -n "$DB_PWD" ]; then + export PGPASSWORD=$DB_PWD + fi + + PSQL="psql -q -h$DB_HOST -p$DB_PORT -d$DB_NAME -U$DB_USER -w" + + $PSQL -f "$APP_DIR/server/schema/postgresql/removetbl.sql" + $PSQL -f "$APP_DIR/server/schema/postgresql/createdb.sql" +} + +upgrade_mysql_tbl() { + CONNECTION_PARAMS="-h$DB_HOST -P$DB_PORT -u$DB_USER -p$DB_PWD -w" + MYSQL="mysql -q $CONNECTION_PARAMS" + + $MYSQL $DB_NAME < "$APP_DIR/server/schema/mysql/removetbl.sql" >/dev/null 2>&1 + $MYSQL $DB_NAME < "$APP_DIR/server/schema/mysql/createdb.sql" >/dev/null 2>&1 +} + +upgrade_mssql_tbl() { + CONN_PARAMS="-U $DB_USER -P "$DB_PWD" -C" + MSSQL="/opt/mssql-tools18/bin/sqlcmd -S $DB_HOST,$DB_PORT $CONN_PARAMS" + + $MSSQL < "$APP_DIR/server/schema/mssql/removetbl.sql" >/dev/null 2>&1 + $MSSQL < "$APP_DIR/server/schema/mssql/createdb.sql" >/dev/null 2>&1 +} + +upgrade_oracle_tbl() { + PDB="XEPDB1" + ORACLE_SQL="sqlplus $DB_USER/$DB_PWD@//$DB_HOST:$DB_PORT/$PDB" + + $ORACLE_SQL @$APP_DIR/server/schema/oracle/removetbl.sql >/dev/null 2>&1 + $ORACLE_SQL @$APP_DIR/server/schema/oracle/createdb.sql >/dev/null 2>&1 +} + +create_postgresql_tbl() { + if [ -n "$DB_PWD" ]; then + export PGPASSWORD=$DB_PWD + fi + + PSQL="psql -q -h$DB_HOST -p$DB_PORT -d$DB_NAME -U$DB_USER -w" + $PSQL -f "$APP_DIR/server/schema/postgresql/createdb.sql" +} + +create_mysql_tbl() { + CONNECTION_PARAMS="-h$DB_HOST -P$DB_PORT -u$DB_USER -p$DB_PWD -w" + MYSQL="mysql -q $CONNECTION_PARAMS" + + # Create db on remote server + $MYSQL -e "CREATE DATABASE IF NOT EXISTS $DB_NAME DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;" >/dev/null 2>&1 + + $MYSQL $DB_NAME < "$APP_DIR/server/schema/mysql/createdb.sql" >/dev/null 2>&1 +} + +create_mssql_tbl() { + create_mssql_db + + CONN_PARAMS="-U $DB_USER -P "$DB_PWD" -C" + MSSQL="/opt/mssql-tools18/bin/sqlcmd -S $DB_HOST,$DB_PORT $CONN_PARAMS" + + $MSSQL < "$APP_DIR/server/schema/mssql/createdb.sql" >/dev/null 2>&1 +} + +create_oracle_tbl() { + PDB="XEPDB1" + ORACLE_SQL="sqlplus $DB_USER/$DB_PWD@//$DB_HOST:$DB_PORT/$PDB" + + $ORACLE_SQL @$APP_DIR/server/schema/oracle/createdb.sql >/dev/null 2>&1 +} + +update_welcome_page() { + WELCOME_PAGE="${APP_DIR}-example/welcome/docker.html" + if [[ -e $WELCOME_PAGE ]]; then + DOCKER_CONTAINER_ID=$(basename $(cat /proc/1/cpuset)) + (( ${#DOCKER_CONTAINER_ID} < 12 )) && DOCKER_CONTAINER_ID=$(hostname) + if (( ${#DOCKER_CONTAINER_ID} >= 12 )); then + if [[ -x $(command -v docker) ]]; then + DOCKER_CONTAINER_NAME=$(docker inspect --format="{{.Name}}" $DOCKER_CONTAINER_ID) + sed 's/$(sudo docker ps -q)/'"${DOCKER_CONTAINER_NAME#/}"'/' -i $WELCOME_PAGE + JWT_MESSAGE=$(echo $JWT_MESSAGE | sed 's/$(sudo docker ps -q)/'"${DOCKER_CONTAINER_NAME#/}"'/') + else + sed 's/$(sudo docker ps -q)/'"${DOCKER_CONTAINER_ID::12}"'/' -i $WELCOME_PAGE + JWT_MESSAGE=$(echo $JWT_MESSAGE | sed 's/$(sudo docker ps -q)/'"${DOCKER_CONTAINER_ID::12}"'/') + fi + fi + fi +} + +update_nginx_settings(){ + # Set up nginx + sed 's/^worker_processes.*/'"worker_processes ${NGINX_WORKER_PROCESSES};"'/' -i ${NGINX_CONFIG_PATH} + sed 's/worker_connections.*/'"worker_connections ${NGINX_WORKER_CONNECTIONS};"'/' -i ${NGINX_CONFIG_PATH} + sed 's/access_log.*/'"access_log off;"'/' -i ${NGINX_CONFIG_PATH} + + # setup HTTPS + if [ -f "${SSL_CERTIFICATE_PATH}" -a -f "${SSL_KEY_PATH}" ]; then + cp -f ${NGINX_ONLYOFFICE_PATH}/ds-ssl.conf.tmpl ${NGINX_ONLYOFFICE_CONF} + + # configure nginx + sed 's,{{SSL_CERTIFICATE_PATH}},'"${SSL_CERTIFICATE_PATH}"',' -i ${NGINX_ONLYOFFICE_CONF} + sed 's,{{SSL_KEY_PATH}},'"${SSL_KEY_PATH}"',' -i ${NGINX_ONLYOFFICE_CONF} + + # turn on http2 + sed 's,\(443 ssl\),\1 http2,' -i ${NGINX_ONLYOFFICE_CONF} + + # if dhparam path is valid, add to the config, otherwise remove the option + if [ -r "${SSL_DHPARAM_PATH}" ]; then + sed 's,\(\#* *\)\?\(ssl_dhparam \).*\(;\)$,'"\2${SSL_DHPARAM_PATH}\3"',' -i ${NGINX_ONLYOFFICE_CONF} + else + sed '/ssl_dhparam/d' -i ${NGINX_ONLYOFFICE_CONF} + fi + + sed 's,\(ssl_verify_client \).*\(;\)$,'"\1${SSL_VERIFY_CLIENT}\2"',' -i ${NGINX_ONLYOFFICE_CONF} + + if [ -f "${CA_CERTIFICATES_PATH}" ]; then + sed '/ssl_verify_client/a '"ssl_client_certificate ${CA_CERTIFICATES_PATH}"';' -i ${NGINX_ONLYOFFICE_CONF} + fi + + if [ "${ONLYOFFICE_HTTPS_HSTS_ENABLED}" == "true" ]; then + sed 's,\(max-age=\).*\(;\)$,'"\1${ONLYOFFICE_HTTPS_HSTS_MAXAGE}\2"',' -i ${NGINX_ONLYOFFICE_CONF} + else + sed '/max-age=/d' -i ${NGINX_ONLYOFFICE_CONF} + fi + else + ln -sf ${NGINX_ONLYOFFICE_PATH}/ds.conf.tmpl ${NGINX_ONLYOFFICE_CONF} + fi + + # check if ipv6 supported otherwise remove it from nginx config + if [ ! -f /proc/net/if_inet6 ]; then + sed '/listen\s\+\[::[0-9]*\].\+/d' -i $NGINX_ONLYOFFICE_CONF + fi + + if [ -f "${NGINX_ONLYOFFICE_EXAMPLE_CONF}" ]; then + sed 's/linux/docker/' -i ${NGINX_ONLYOFFICE_EXAMPLE_CONF} + fi + + start_process documentserver-update-securelink.sh -s ${SECURE_LINK_SECRET:-$(pwgen -s 20)} -r false +} + +update_log_settings(){ + ${JSON_LOG} -I -e "this.categories.default.level = '${DS_LOG_LEVEL}'" +} + +update_logrotate_settings(){ + sed 's|\(^su\b\).*|\1 root root|' -i /etc/logrotate.conf +} + +update_release_date(){ + mkdir -p ${PRIVATE_DATA_DIR} + echo ${RELEASE_DATE} > ${DS_RELEASE_DATE} +} + +# create base folders +for i in converter docservice metrics; do + mkdir -p "${DS_LOG_DIR}/$i" +done + +mkdir -p ${DS_LOG_DIR}-example + +# create app folders +for i in ${DS_LIB_DIR}/App_Data/cache/files ${DS_LIB_DIR}/App_Data/docbuilder ${DS_LIB_DIR}-example/files; do + mkdir -p "$i" +done + +# change folder rights +for i in ${DS_LOG_DIR} ${DS_LOG_DIR}-example ${LIB_DIR}; do + chown -R ds:ds "$i" + chmod -R 755 "$i" +done + +if [ ${ONLYOFFICE_DATA_CONTAINER_HOST} = "localhost" ]; then + + read_setting + + if [ $METRICS_ENABLED = "true" ]; then + update_statsd_settings + fi + + update_welcome_page + + update_log_settings + + update_ds_settings + + # update settings by env variables + if [ $DB_HOST != "localhost" ]; then + update_db_settings + waiting_for_db + create_db_tbl + else + # change rights for postgres directory + chown -R postgres:postgres ${PG_ROOT} + chmod -R 700 ${PG_ROOT} + + # create new db if it isn't exist + if [ ! -d ${PGDATA} ]; then + create_postgresql_cluster + PG_NEW_CLUSTER=true + fi + LOCAL_SERVICES+=("postgresql") + fi + + if [ ${AMQP_SERVER_HOST} != "localhost" ]; then + update_rabbitmq_setting + else + # change rights for rabbitmq directory + chown -R rabbitmq:rabbitmq ${RABBITMQ_DATA} + chmod -R go=rX,u=rwX ${RABBITMQ_DATA} + if [ -f ${RABBITMQ_DATA}/.erlang.cookie ]; then + chmod 400 ${RABBITMQ_DATA}/.erlang.cookie + fi + + echo "ulimit -n $RABBIT_CONNECTIONS" >> /etc/default/rabbitmq-server + + LOCAL_SERVICES+=("rabbitmq-server") + # allow Rabbitmq startup after container kill + rm -rf /var/run/rabbitmq + fi + + if [ ${REDIS_ENABLED} = "true" ]; then + if [ ${REDIS_SERVER_HOST} != "localhost" ]; then + update_redis_settings + else + # change rights for redis directory + chown -R redis:redis ${REDIS_DATA} + chmod -R 750 ${REDIS_DATA} + + LOCAL_SERVICES+=("redis-server") + fi + fi +else + # no need to update settings just wait for remote data + waiting_for_datacontainer + + # read settings after the data container in ready state + # to prevent get unconfigureted data + read_setting + + update_welcome_page +fi + +find /etc/${COMPANY_NAME} ! -path '*logrotate*' -exec chown ds:ds {} \; + +#start needed local services +for i in ${LOCAL_SERVICES[@]}; do + service $i start +done + +if [ ${PG_NEW_CLUSTER} = "true" ]; then + create_postgresql_db + create_postgresql_tbl +fi + +if [ ${ONLYOFFICE_DATA_CONTAINER} != "true" ]; then + waiting_for_db + waiting_for_amqp + if [ ${REDIS_ENABLED} = "true" ]; then + waiting_for_redis + fi + + if [ "${IS_UPGRADE}" = "true" ]; then + upgrade_db_tbl + update_release_date + fi + + update_nginx_settings + + service supervisor start + + # start cron to enable log rotating + update_logrotate_settings + service cron start +fi + +# Fix to resolve the `unknown "cache_tag" variable` error +start_process documentserver-flush-cache.sh -r false + +# nginx used as a proxy, and as data container status service. +# it run in all cases. +service nginx start + +if [ "${LETS_ENCRYPT_DOMAIN}" != "" -a "${LETS_ENCRYPT_MAIL}" != "" ]; then + if [ ! -f "${SSL_CERTIFICATE_PATH}" -a ! -f "${SSL_KEY_PATH}" ]; then + start_process documentserver-letsencrypt.sh ${LETS_ENCRYPT_MAIL} ${LETS_ENCRYPT_DOMAIN} + fi +fi + +# Regenerate the fonts list and the fonts thumbnails +if [ "${GENERATE_FONTS}" == "true" ]; then + start_process documentserver-generate-allfonts.sh ${ONLYOFFICE_DATA_CONTAINER} +fi + +if [ "${PLUGINS_ENABLED}" = "true" ]; then + echo -n Installing plugins, please wait... + start_process documentserver-pluginsmanager.sh -r false --update=\"${APP_DIR}/sdkjs-plugins/plugin-list-default.json\" >/dev/null + echo Done +fi + +start_process documentserver-static-gzip.sh ${ONLYOFFICE_DATA_CONTAINER} + +echo "${JWT_MESSAGE}" + +start_process tail -f /var/log/${COMPANY_NAME}/**/*.log diff --git a/onlyoffice/tests/README.md b/onlyoffice/tests/README.md new file mode 100644 index 0000000..c17b546 --- /dev/null +++ b/onlyoffice/tests/README.md @@ -0,0 +1,3 @@ +The files in this folder are intended for use in integration auto-tests. + +All credentials are strictly for testing purposes only. \ No newline at end of file diff --git a/onlyoffice/tests/activemq.yml b/onlyoffice/tests/activemq.yml new file mode 100644 index 0000000..5781c64 --- /dev/null +++ b/onlyoffice/tests/activemq.yml @@ -0,0 +1,32 @@ +version: '2.1' +services: + onlyoffice-documentserver: + container_name: onlyoffice-documentserver + build: + context: ../. + environment: + - AMQP_TYPE=${AMQP_TYPE:-activemq} + - AMQP_URI=${AMQP_URI:-amqp://guest:guest@onlyoffice-activemq} + stdin_open: true + restart: always + ports: + - '80:80' + - '443:443' + networks: + - onlyoffice + + onlyoffice-activemq: + container_name: onlyoffice-activemq + image: webcenter/activemq:${ACTIVEMQ_VERSION:-5.14.3} + environment: + - ACTIVEMQ_USERS_guest=${ACTIVEMQ_USERS_guest:-guest} + - ACTIVEMQ_GROUPS_owners=${ACTIVEMQ_GROUPS_owners:-guest} + restart: always + networks: + - onlyoffice + expose: + - '5672' + +networks: + onlyoffice: + driver: 'bridge' diff --git a/onlyoffice/tests/certs-customized.yml b/onlyoffice/tests/certs-customized.yml new file mode 100644 index 0000000..af73081 --- /dev/null +++ b/onlyoffice/tests/certs-customized.yml @@ -0,0 +1,18 @@ +version: '2.1' +services: + onlyoffice-documentserver: + container_name: onlyoffice-documentserver + build: + context: ../. + environment: + - SSL_CERTIFICATE_PATH=${SSL_CERTIFICATE_PATH:-/var/www/onlyoffice/Data/certs/tls.crt} + - SSL_KEY_PATH=${SSL_KEY_PATH:-/var/www/onlyoffice/Data/certs/tls.key} + - CA_CERTIFICATES_PATH=${CA_CERTIFICATES_PATH:-/var/www/onlyoffice/Data/certs/ca-certificates.pem} + - SSL_DHPARAM_PATH=${SSL_DHPARAM_PATH:-/var/www/onlyoffice/Data/certs/dhparam.pem} + stdin_open: true + restart: always + ports: + - '80:80' + - '443:443' + volumes: + - ./data:/var/www/onlyoffice/Data diff --git a/onlyoffice/tests/certs.yml b/onlyoffice/tests/certs.yml new file mode 100644 index 0000000..77d37ce --- /dev/null +++ b/onlyoffice/tests/certs.yml @@ -0,0 +1,13 @@ +version: '2.1' +services: + onlyoffice-documentserver: + container_name: onlyoffice-documentserver + build: + context: ../. + stdin_open: true + restart: always + ports: + - '80:80' + - '443:443' + volumes: + - ./data:/var/www/onlyoffice/Data diff --git a/onlyoffice/tests/damengdb/README.md b/onlyoffice/tests/damengdb/README.md new file mode 100644 index 0000000..c7f9c97 --- /dev/null +++ b/onlyoffice/tests/damengdb/README.md @@ -0,0 +1,19 @@ +## Stand Documentserver with damengdb + +### How it works + +For deploy stand, you need: + +**STEP 1**: Build you own images, do it with command: + +```bash +docker compose build +``` + +**STEP 2**: Wait build and when it finish deploy with command: + +```bash +docker compose up -d +``` + +Thats all. diff --git a/onlyoffice/tests/damengdb/damengdb.Dockerfile b/onlyoffice/tests/damengdb/damengdb.Dockerfile new file mode 100644 index 0000000..f4f6cf1 --- /dev/null +++ b/onlyoffice/tests/damengdb/damengdb.Dockerfile @@ -0,0 +1,57 @@ +FROM onlyoffice/damengdb:8.1.2 as damengdb + +ARG DM8_USER="SYSDBA" +ARG DM8_PASS="SYSDBA001" +ARG DB_HOST="localhost" +ARG DB_PORT="5236" +ARG DISQL_BIN="/opt/dmdbms/bin" + +SHELL ["/bin/bash", "-c"] + +COPY <<"EOF" /wait_dm_ready.sh +#!/usr/bin/env bash + +function wait_dm_ready() { + cd /opt/dmdbms/bin + for i in `seq 1 10`; do + echo `./disql /nolog < /dev/null 2>&1 + if [ $? -eq 0 ]; then + echo "DM Database is not OK, please wait..." + sleep 10 + else + echo "DM Database is OK" + break + fi + done +} + +wait_dm_ready + +EOF + +COPY <<"EOF" /permissions.sql + +CREATE SYNONYM onlyoffice.DOC_CHANGES FOR sysdba.DOC_CHANGES; +CREATE SYNONYM onlyoffice.TASK_RESULT FOR sysdba.TASK_RESULT; +GRANT ALL PRIVILEGES ON sysdba.DOC_CHANGES TO onlyoffice; +GRANT ALL PRIVILEGES ON sysdba.TASK_RESULT TO onlyoffice; + +EOF + +RUN bash /opt/startup.sh > /dev/null 2>&1 \ + & mkdir -p /schema/damengdb \ + && apt update -y ; apt install wget -y \ + && wget https://raw.githubusercontent.com/ONLYOFFICE/server/master/schema/dameng/createdb.sql -P /schema/dameng/ \ + && bash ./wait_dm_ready.sh \ + && cd ${DISQL_BIN} \ + && ./disql $DM8_USER/$DM8_PASS@$DB_HOST:$DB_PORT -e \ + "create user "onlyoffice" identified by "onlyoffice" password_policy 0;" \ + && ./disql $DM8_USER/$DM8_PASS@$DB_HOST:$DB_PORT -e \ + "GRANT SELECT ON DBA_TAB_COLUMNS TO onlyoffice;" \ + && echo "EXIT" | tee -a /schema/dameng/createdb.sql \ + && ./disql $DM8_USER/$DM8_PASS@$DB_HOST:$DB_PORT \`/schema/dameng/createdb.sql \ + && ./disql $DM8_USER/$DM8_PASS@$DB_HOST:$DB_PORT \`/permissions.sql \ + && sleep 10 diff --git a/onlyoffice/tests/damengdb/docker-compose.yml b/onlyoffice/tests/damengdb/docker-compose.yml new file mode 100644 index 0000000..c28dfb6 --- /dev/null +++ b/onlyoffice/tests/damengdb/docker-compose.yml @@ -0,0 +1,67 @@ +version: '2' +services: + onlyoffice-documentserver: + build: + context: ../../. + dockerfile: Dockerfile + target: documentserver + container_name: onlyoffice-documentserver + depends_on: + - onlyoffice-dameng + - onlyoffice-rabbitmq + environment: + - DB_TYPE=dameng + - DB_HOST=onlyoffice-dameng + - DB_PORT=5236 + - DB_NAME=onlyoffice + - DB_USER=onlyoffice + - AMQP_URI=amqp://guest:guest@onlyoffice-rabbitmq + # Costomize the JSON Web Token validation parameters if needed. + #- JWT_ENABLED=false + #- JWT_SECRET=secret + #- JWT_HEADER=Authorization + #- JWT_IN_BODY=true + ports: + - '80:80' + - '443:443' + stdin_open: true + restart: always + stop_grace_period: 60s + volumes: + - /var/www/onlyoffice/Data + - /var/log/onlyoffice + - /var/lib/onlyoffice/documentserver/App_Data/cache/files + - /var/www/onlyoffice/documentserver-example/public/files + - /usr/share/fonts + + onlyoffice-rabbitmq: + container_name: onlyoffice-rabbitmq + image: rabbitmq + restart: always + expose: + - '5672' + + onlyoffice-dameng: + container_name: onlyoffice-dameng + build: + context: . + dockerfile: damengdb.Dockerfile + target: damengdb + args: + DM8_USER: SYSDBA + DM8_PASS: SYSDBA001 + DB_HOST: localhost + DB_PORT: 5236 + environment: + - PAGE_SIZE=16 + - LD_LIBRARY_PATH=/opt/dmdbms/bin + - INSTANCE_NAME=dm8_01 + restart: always + expose: + - '5236' + volumes: + - dameng_data:/opt/dmdbms/data + +volumes: + dameng_data: + diff --git a/onlyoffice/tests/graphite.yml b/onlyoffice/tests/graphite.yml new file mode 100644 index 0000000..2bc4694 --- /dev/null +++ b/onlyoffice/tests/graphite.yml @@ -0,0 +1,32 @@ +version: '2.1' +services: + onlyoffice-documentserver: + container_name: onlyoffice-documentserver + build: + context: ../. + depends_on: + - onlyoffice-graphite + environment: + - METRICS_ENABLED=${METRICS_ENABLED:-true} + - METRICS_HOST=${METRICS_HOST:-localhost} + - METRICS_PORT=${METRICS_PORT:-8125} + - METRICS_PREFIX=${METRICS_PREFIX:-ds.} + stdin_open: true + restart: always + expose: + - '2003' + ports: + - '80:80' + volumes: + - ./graphite/statsd:/var/www/onlyoffice/documentserver/server/Metrics/config + + onlyoffice-graphite: + container_name: onlyoffice-graphite + image: graphiteapp/graphite-statsd + environment: + - GRAPHITE_STATSD_HOST=${GRAPHITE_STATSD_HOST:-onlyoffice-documentserver} + - GRAPHITE_TIME_ZONE=${GRAPHITE_TIME_ZONE:-Etc/UTC} + ports: + - '8888:80' + stdin_open: true + restart: always diff --git a/onlyoffice/tests/graphite/statsd/config.js b/onlyoffice/tests/graphite/statsd/config.js new file mode 100644 index 0000000..2ebffe6 --- /dev/null +++ b/onlyoffice/tests/graphite/statsd/config.js @@ -0,0 +1,7 @@ +{ + "graphiteHost": "onlyoffice-graphite", + "graphitePort": 2003, + "port": 8125, + "flushInterval": 60000, + "backends": [ "./backends/graphite.js" ] +} diff --git a/onlyoffice/tests/mariadb.yml b/onlyoffice/tests/mariadb.yml new file mode 100644 index 0000000..4bb8de5 --- /dev/null +++ b/onlyoffice/tests/mariadb.yml @@ -0,0 +1,36 @@ +version: '2.1' +services: + ds: + container_name: ds + build: + context: ../. + depends_on: + - onlyoffice-mariadb + environment: + - DB_TYPE=${DB_TYPE:-mysql} + - DB_HOST=${DB_HOST:-onlyoffice-mariadb} + - DB_PORT=${DB_PORT:-3306} + - DB_NAME=${DB_NAME:-onlyoffice} + - DB_USER=${DB_USER:-onlyoffice} + - DB_PWD=${DB_PWD:-onlyoffice} + stdin_open: true + restart: always + ports: + - '80:80' + + onlyoffice-mariadb: + container_name: onlyoffice-mariadb + image: mariadb:${MARIADB_VERSION:-10.5} + environment: + - MYSQL_DATABASE=${MYSQL_DATABASE:-onlyoffice} + - MYSQL_USER=${MYSQL_USER:-onlyoffice} + - MYSQL_PASSWORD=${MYSQL_PASSWORD:-onlyoffice} + - MYSQL_ALLOW_EMPTY_PASSWORD=${MYSQL_ALLOW_EMPTY_PASSWORD:-yes} + restart: always + volumes: + - mysql_data:/var/lib/mysql + expose: + - '3306' + +volumes: + mysql_data: diff --git a/onlyoffice/tests/mssql/README.md b/onlyoffice/tests/mssql/README.md new file mode 100644 index 0000000..3db562f --- /dev/null +++ b/onlyoffice/tests/mssql/README.md @@ -0,0 +1,17 @@ +## Stand Documentserver with mssql + +### How it works + +For deploy stand: + +**STEP 1**: Build you own images: + +```bash +sudo docker-compose build +``` + +**STEP 2**: Wait build complete and when: + +```bash +sudo docker-compose up -d +``` diff --git a/onlyoffice/tests/mssql/create_db_user.sh b/onlyoffice/tests/mssql/create_db_user.sh new file mode 100755 index 0000000..cbe4c4e --- /dev/null +++ b/onlyoffice/tests/mssql/create_db_user.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +#generate SA password +SYMBOLS='!#$%&*+,-.:;=?@^_~' +for (( i=1; i <= 20; i++ )); do + PASS=$(tr -dc "A-Za-z0-9$SYMBOLS" /dev/null | grep "affected" | wc -l) + if [ "$RES" -eq "1" ]; then + echo "Database is ready" + break + fi + sleep 10 +done + +#create new db user +$CONNECTION_STR "IF NOT EXISTS (SELECT * FROM sys.sql_logins WHERE name = '$MSSQL_USER') BEGIN CREATE LOGIN $MSSQL_USER WITH PASSWORD = '$MSSQL_PASSWORD' , CHECK_POLICY = OFF; ALTER SERVER ROLE [dbcreator] ADD MEMBER [$MSSQL_USER]; END" diff --git a/onlyoffice/tests/mssql/docker-compose.yml b/onlyoffice/tests/mssql/docker-compose.yml new file mode 100644 index 0000000..036bbe5 --- /dev/null +++ b/onlyoffice/tests/mssql/docker-compose.yml @@ -0,0 +1,38 @@ +version: '2.1' +services: + onlyoffice-documentserver: + container_name: onlyoffice-documentserver + build: + context: ../../. + dockerfile: Dockerfile + depends_on: + - onlyoffice-mssql + environment: + - DB_TYPE=${DB_TYPE:-mssql} + - DB_HOST=${DB_HOST:-onlyoffice-mssql} + - DB_PORT=${DB_PORT:-1433} + - DB_NAME=${DB_NAME:-onlyoffice} + - DB_USER=${DB_USER:-onlyoffice} + - DB_PWD=${DB_PWD:-onlyoffice} + stdin_open: true + restart: always + ports: + - '80:80' + + onlyoffice-mssql: + container_name: onlyoffice-mssql + build: + context: . + dockerfile: mssql.Dockerfile + args: + - MSSQL_DATABASE=${DB_NAME:-onlyoffice} + - MSSQL_USER=${DB_USER:-onlyoffice} + - MSSQL_PASSWORD=${DB_PWD:-onlyoffice} + restart: always + volumes: + - mssql_data:/var/opt/mssql + expose: + - '1433' + +volumes: + mssql_data: diff --git a/onlyoffice/tests/mssql/mssql.Dockerfile b/onlyoffice/tests/mssql/mssql.Dockerfile new file mode 100644 index 0000000..f6dddc6 --- /dev/null +++ b/onlyoffice/tests/mssql/mssql.Dockerfile @@ -0,0 +1,9 @@ +FROM mcr.microsoft.com/mssql/server:2022-latest as onlyoffice-mssql + +ENV ACCEPT_EULA=Y + +SHELL ["/bin/bash", "-c"] + +COPY create_db_user.sh /tmp/create_db_user.sh + +RUN bash /tmp/create_db_user.sh diff --git a/onlyoffice/tests/mysql.yml b/onlyoffice/tests/mysql.yml new file mode 100644 index 0000000..20fcd70 --- /dev/null +++ b/onlyoffice/tests/mysql.yml @@ -0,0 +1,37 @@ +version: '2.1' +services: + onlyoffice-documentserver: + container_name: onlyoffice-documentserver + build: + context: ../. + depends_on: + - onlyoffice-mysql + environment: + - DB_TYPE=${DB_TYPE:-mysql} + - DB_HOST=${DB_HOST:-onlyoffice-mysql} + - DB_PORT=${DB_PORT:-3306} + - DB_NAME=${DB_NAME:-onlyoffice} + - DB_USER=${DB_USER:-onlyoffice} + - DB_PWD=${DB_PWD:-onlyoffice} + stdin_open: true + restart: always + ports: + - '80:80' + + onlyoffice-mysql: + container_name: onlyoffice-mysql + image: mysql:${MYSQL_VERSION:-5.7} + command: --default-authentication-plugin=mysql_native_password + environment: + - MYSQL_DATABASE=${MYSQL_DATABASE:-onlyoffice} + - MYSQL_USER=${MYSQL_USER:-onlyoffice} + - MYSQL_PASSWORD=${MYSQL_PASSWORD:-onlyoffice} + - MYSQL_ALLOW_EMPTY_PASSWORD=${MYSQL_ALLOW_EMPTY_PASSWORD:-yes} + restart: always + volumes: + - mysql_data:/var/lib/mysql + expose: + - '3306' + +volumes: + mysql_data: diff --git a/onlyoffice/tests/oracle/README.md b/onlyoffice/tests/oracle/README.md new file mode 100644 index 0000000..f548805 --- /dev/null +++ b/onlyoffice/tests/oracle/README.md @@ -0,0 +1,17 @@ +## Stand Documentserver with oracle + +### How it works + +For deploy stand: + +**STEP 1**: Build you own images: + +```bash +sudo docker-compose build +``` + +**STEP 2**: Wait build complete and when: + +```bash +sudo docker-compose up -d +``` diff --git a/onlyoffice/tests/oracle/create_db_user.sh b/onlyoffice/tests/oracle/create_db_user.sh new file mode 100755 index 0000000..831df42 --- /dev/null +++ b/onlyoffice/tests/oracle/create_db_user.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +CONNECTION_STR="sqlplus sys/$ORACLE_PASSWORD@//localhost:1521/$ORACLE_DATABASE as sysdba" + +export ORACLE_PWD=$ORACLE_PASSWORD + +#start db +/opt/oracle/runOracle.sh & + +#wait for db up +for (( i=1; i <= 20; i++ )); do + RES=$(echo "SELECT version FROM V\$INSTANCE;" | $CONNECTION_STR 2>/dev/null | grep "Connected" | wc -l) + if [ "$RES" -ne "0" ]; then + echo "Database is ready" + break + fi + sleep 10 +done + +sleep 1 + +#create new db user +$CONNECTION_STR < an unique provider name +- name: 'default-provider' + # org id. will default to orgId 1 if not specified + orgId: 1 + # name of the dashboard folder. Required + folder: dashboards + # folder UID. will be automatically generated if not specified + folderUid: '' + # provider type. Required + type: file + # disable dashboard deletion + disableDeletion: false + # enable dashboard editing + editable: true + # how often Grafana will scan for changed dashboards + updateIntervalSeconds: 10 + options: + # path to dashboard files on disk. Required + path: /opt/bitnami/grafana/dashboards + # enable folders creation for dashboards + #foldersFromFilesStructure: true diff --git a/onlyoffice/tests/prometheus/grafana/conf/prometheus.yml b/onlyoffice/tests/prometheus/grafana/conf/prometheus.yml new file mode 100644 index 0000000..11428bc --- /dev/null +++ b/onlyoffice/tests/prometheus/grafana/conf/prometheus.yml @@ -0,0 +1,6 @@ +apiVersion: 1 +datasources: + - name: Prometheus + type: prometheus + url: http://onlyoffice-prometheus:9090 + editable: true diff --git a/onlyoffice/tests/prometheus/grafana/dashboards/documentserver-statsd-exporter.json b/onlyoffice/tests/prometheus/grafana/dashboards/documentserver-statsd-exporter.json new file mode 100644 index 0000000..d7a7c6f --- /dev/null +++ b/onlyoffice/tests/prometheus/grafana/dashboards/documentserver-statsd-exporter.json @@ -0,0 +1,2797 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 8, + "links": [], + "panels": [ + { + "collapsed": true, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 22, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "none" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 1 + }, + "hiddenSeries": false, + "id": 76, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "ds_expireDoc_connections_edit", + "interval": "", + "legendFormat": "number of connections for editing", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Edit", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "none", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "none" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 1 + }, + "hiddenSeries": false, + "id": 78, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "ds_expireDoc_connections_view", + "interval": "", + "legendFormat": "number of connections for viewing", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "View", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "none", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "none" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 1 + }, + "hiddenSeries": false, + "id": 80, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "ds_expireDoc_connections_edit + ds_expireDoc_connections_view", + "interval": "", + "legendFormat": "sum of connections for editing and viewing", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Sum", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "none", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "title": "Сonnecting", + "type": "row" + }, + { + "collapsed": true, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 1 + }, + "id": 56, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "s" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 2 + }, + "hiddenSeries": false, + "id": 52, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(ds_coauth_openDocument_open_sum[5m])/rate(ds_coauth_openDocument_open_count[5m])", + "interval": "", + "legendFormat": "moving average time of opening documents (for 5 minutes)", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Average time", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "s" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 2 + }, + "hiddenSeries": false, + "id": 54, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "ds_coauth_openDocument_open", + "interval": "", + "legendFormat": "quantile=\"{{quantile}}\"", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Quantile (0.5, 0.9, 0.99)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "cpm" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 2 + }, + "hiddenSeries": false, + "id": 74, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "ds_coauth_openDocument_open_count - ds_coauth_openDocument_open_count offset 1m", + "interval": "", + "legendFormat": "number of open documents", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Count", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "cpm", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "title": "Opening Documents", + "type": "row" + }, + { + "collapsed": true, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 2 + }, + "id": 10, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "color": {}, + "custom": {}, + "thresholds": { + "mode": "absolute", + "steps": [] + }, + "unit": "s" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 3 + }, + "hiddenSeries": false, + "id": 8, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(ds_conv_downloadFile_sum[5m])/rate(ds_conv_downloadFile_count[5m])", + "interval": "", + "legendFormat": "moving average time of downloading documents (for 5 minutes)", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Average time", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "s" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 3 + }, + "hiddenSeries": false, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "ds_conv_downloadFile", + "interval": "", + "legendFormat": "quantile=\"{{quantile}}\"", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Quantile (0.5, 0.9, 0.99)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "cpm" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 3 + }, + "hiddenSeries": false, + "id": 6, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "ds_conv_downloadFile_count - ds_conv_downloadFile_count offset 1m", + "interval": "", + "legendFormat": "number of downloaded files", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Count", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "cpm", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "title": "Downloading Documents", + "type": "row" + }, + { + "collapsed": true, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 3 + }, + "id": 12, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "s" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 4 + }, + "hiddenSeries": false, + "id": 16, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(ds_conv_allconvert_sum[5m])/rate(ds_conv_allconvert_count[5m])", + "interval": "", + "legendFormat": "moving average time of converting documents (for 5 minutes)", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Average time", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "s" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 4 + }, + "hiddenSeries": false, + "id": 18, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "ds_conv_allconvert", + "interval": "", + "legendFormat": "quantile=\"{{quantile}}\"", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "quantile (0.5, 0.9, 0.99)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "cpm" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 4 + }, + "hiddenSeries": false, + "id": 14, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "ds_conv_allconvert_count - ds_conv_allconvert_count offset 1m", + "interval": "", + "legendFormat": "number of conversions", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Count", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "cpm", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "title": "Converting Documents", + "type": "row" + }, + { + "collapsed": true, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 4 + }, + "id": 32, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "s" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 5 + }, + "hiddenSeries": false, + "id": 30, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(ds_conv_spawnSync_sum[5m])/rate(ds_conv_spawnSync_count[5m])", + "interval": "", + "legendFormat": "moving average time of converting process (for 5 minutes)", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Average time", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "s" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 5 + }, + "hiddenSeries": false, + "id": 28, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "ds_conv_spawnSync", + "interval": "", + "legendFormat": "quantile=\"{{quantile}}\"", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Quantile (0.5, 0.9, 0.99)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "cpm" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 5 + }, + "hiddenSeries": false, + "id": 26, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "ds_conv_spawnSync_count - ds_conv_spawnSync_count offset 1m", + "interval": "", + "legendFormat": "number of conversion process", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Count", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "cpm", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "title": "Converting Process", + "type": "row" + }, + { + "collapsed": true, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "id": 48, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "s" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 6 + }, + "hiddenSeries": false, + "id": 44, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(ds_coauth_data_auth_sum[5m])/rate(ds_coauth_data_auth_count[5m])", + "interval": "", + "legendFormat": "moving average time of completing authorization (for 5 minutes)", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Average time", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "s" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 6 + }, + "hiddenSeries": false, + "id": 46, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "ds_coauth_data_auth", + "interval": "", + "legendFormat": "quantile=\"{{quantile}}\"", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Quantile (0.5, 0.9, 0.99)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "cpm" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 6 + }, + "hiddenSeries": false, + "id": 42, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "ds_coauth_data_auth_count - ds_coauth_data_auth_count offset 1m", + "interval": "", + "legendFormat": "number of authorizations", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Count", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "cpm", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "title": "Authorizations", + "type": "row" + }, + { + "collapsed": true, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 6 + }, + "id": 64, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "s" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 7 + }, + "hiddenSeries": false, + "id": 60, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(ds_coauth_data_getLock_sum[5m])/rate(ds_coauth_data_getLock_count[5m])", + "interval": "", + "legendFormat": "moving average time of getLock duration (for 5 minutes)", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Average time", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "s" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 7 + }, + "hiddenSeries": false, + "id": 62, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "ds_coauth_data_getLock", + "interval": "", + "legendFormat": "quantile=\"{{quantile}}\"", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Quantile (0.5, 0.9, 0.99)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "cpm" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 7 + }, + "hiddenSeries": false, + "id": 58, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "ds_coauth_data_getLock_count - ds_coauth_data_getLock_count offset 1m", + "interval": "", + "legendFormat": "number of getLocks", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Count", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "cpm", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "title": "Get Lock", + "type": "row" + }, + { + "collapsed": true, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 7 + }, + "id": 40, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "s" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 8 + }, + "hiddenSeries": false, + "id": 36, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(ds_coauth_data_saveChanges_sum[5m])/rate(ds_coauth_data_saveChanges_count[5m])", + "interval": "", + "legendFormat": "moving average time of saving changes (for 5 minutes)", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Average time", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "s" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 8 + }, + "hiddenSeries": false, + "id": 38, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "ds_coauth_data_saveChanges", + "interval": "", + "legendFormat": "quantile=\"{{quantile}}\"", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Quantile (0.5, 0.9, 0.99)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "cpm" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 8 + }, + "hiddenSeries": false, + "id": 34, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "ds_coauth_data_saveChanges_count - ds_coauth_data_saveChanges_count offset 1m", + "interval": "", + "legendFormat": "number of saved changes ", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Count", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "cpm", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "title": "Saving Changes", + "type": "row" + }, + { + "collapsed": true, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 72, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "s" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 9 + }, + "hiddenSeries": false, + "id": 70, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(ds_coauth_openDocument_imgurls_sum[5m])/rate(ds_coauth_openDocument_imgurls_count[5m])", + "interval": "", + "legendFormat": "moving average time to opening documents with uploading images (for 5 minutes)", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Average time", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "s" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 9 + }, + "hiddenSeries": false, + "id": 68, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "ds_coauth_openDocument_imgurls", + "interval": "", + "legendFormat": "quantile=\"{{quantile}}\"", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Quantile (0.5, 0.9, 0.99)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "unit": "cpm" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 9 + }, + "hiddenSeries": false, + "id": 66, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.4.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "ds_coauth_openDocument_imgurls_count - ds_coauth_openDocument_imgurls_count offset 1m", + "interval": "", + "legendFormat": "the number of opened documents with the uploaded images", + "queryType": "randomWalk", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Count", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "cpm", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "title": "Uploading Images", + "type": "row" + } + ], + "refresh": "30s", + "schemaVersion": 27, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-3h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Statsd DS", + "uid": "LDjoK2UGz", + "version": 24 +} \ No newline at end of file diff --git a/onlyoffice/tests/prometheus/prometheus-scrape/statsd-exporter.yml b/onlyoffice/tests/prometheus/prometheus-scrape/statsd-exporter.yml new file mode 100644 index 0000000..b3322d0 --- /dev/null +++ b/onlyoffice/tests/prometheus/prometheus-scrape/statsd-exporter.yml @@ -0,0 +1,6 @@ +scrape_configs: + - job_name: 'statsd' + scrape_interval: 30s + static_configs: + - targets: + - onlyoffice-statsd-exporter:9102 diff --git a/onlyoffice/tests/rabbitmq-old.yml b/onlyoffice/tests/rabbitmq-old.yml new file mode 100644 index 0000000..ce18691 --- /dev/null +++ b/onlyoffice/tests/rabbitmq-old.yml @@ -0,0 +1,29 @@ +version: '2.1' +services: + onlyoffice-documentserver: + container_name: onlyoffice-documentserver + build: + context: ../. + environment: + - AMQP_SERVER_TYPE=${AMQP_SERVER_TYPE:-rabbitmq} + - AMQP_SERVER_URL=${AMQP_SERVER_URL:-amqp://guest:guest@onlyoffice-rabbitmq} + stdin_open: true + restart: always + ports: + - '80:80' + - '443:443' + networks: + - onlyoffice + + onlyoffice-rabbitmq: + container_name: onlyoffice-rabbitmq + image: rabbitmq + restart: always + networks: + - onlyoffice + expose: + - '5672' + +networks: + onlyoffice: + driver: 'bridge' diff --git a/onlyoffice/tests/rabbitmq.yml b/onlyoffice/tests/rabbitmq.yml new file mode 100644 index 0000000..293045c --- /dev/null +++ b/onlyoffice/tests/rabbitmq.yml @@ -0,0 +1,29 @@ +version: '2.1' +services: + onlyoffice-documentserver: + container_name: onlyoffice-documentserver + build: + context: ../. + environment: + - AMQP_TYPE=${AMQP_TYPE:-rabbitmq} + - AMQP_URI=${AMQP_URI:-amqp://guest:guest@onlyoffice-rabbitmq} + stdin_open: true + restart: always + ports: + - '80:80' + - '443:443' + networks: + - onlyoffice + + onlyoffice-rabbitmq: + container_name: onlyoffice-rabbitmq + image: rabbitmq:${RABBITMQ_VERSION:-latest} + restart: always + networks: + - onlyoffice + expose: + - '5672' + +networks: + onlyoffice: + driver: 'bridge' diff --git a/onlyoffice/tests/redis.yml b/onlyoffice/tests/redis.yml new file mode 100644 index 0000000..849be16 --- /dev/null +++ b/onlyoffice/tests/redis.yml @@ -0,0 +1,31 @@ +version: '2.1' +services: + onlyoffice-documentserver: + container_name: onlyoffice-documentserver + build: + context: ../. + args: + - PRODUCT_NAME=${PRODUCT_NAME:-documentserver} + environment: + - REDIS_SERVER_HOST=${REDIS_SERVER_HOST:-onlyoffice-redis} + - REDIS_SERVER_PORT=${REDIS_SERVER_PORT:-6379} + stdin_open: true + restart: always + ports: + - '80:80' + - '443:443' + networks: + - onlyoffice + + onlyoffice-redis: + container_name: onlyoffice-redis + image: redis:${REDIS_VERSION:-latest} + restart: always + networks: + - onlyoffice + expose: + - '6379' + +networks: + onlyoffice: + driver: 'bridge' diff --git a/onlyoffice/tests/standalone.yml b/onlyoffice/tests/standalone.yml new file mode 100644 index 0000000..f3de32f --- /dev/null +++ b/onlyoffice/tests/standalone.yml @@ -0,0 +1,12 @@ +version: '2.1' +services: + onlyoffice-documentserver: + container_name: onlyoffice-documentserver + build: + context: ../. + args: + - PRODUCT_NAME=${PRODUCT_NAME:-documentserver} + stdin_open: true + restart: always + ports: + - '80:80' diff --git a/onlyoffice/tests/test.sh b/onlyoffice/tests/test.sh new file mode 100755 index 0000000..535003a --- /dev/null +++ b/onlyoffice/tests/test.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +ssl=${ssl:-false} +private_key=${private_key:-tls.key} +certificate_request=${certificate_request:-tls.csr} +certificate=${certificate:-tls.crt} + +# Generate certificate +if [[ $ssl == "true" ]]; then + url=${url:-"https://localhost"} + + mkdir -p data/certs + pushd data/certs + + openssl genrsa -out ${private_key} 2048 + openssl req \ + -new \ + -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" \ + -key ${private_key} \ + -out ${certificate_request} + openssl x509 -req -days 365 -in ${certificate_request} -signkey ${private_key} -out ${certificate} + openssl dhparam -out dhparam.pem 2048 + chmod 400 ${private_key} + + popd +else + url=${url:-"http://localhost"} +fi + +# Check if the yml exists +if [[ ! -f $config ]]; then + echo "File $config doesn't exist!" + exit 1 +fi + +# Run test environment +docker-compose -p ds -f $config up -d + +wakeup_timeout=90 + +# Get documentserver healthcheck status +echo "Wait for service wake up" +sleep $wakeup_timeout +healthcheck_res=$(wget --no-check-certificate -qO - ${url}/healthcheck) + +# Fail if it isn't true +if [[ $healthcheck_res == "true" ]]; then + echo "Healthcheck passed." +else + echo "Healthcheck failed!" + exit 1 +fi + +docker-compose -p ds -f $config down diff --git a/openproject/.env.example b/openproject/.env.example new file mode 100644 index 0000000..cb62960 --- /dev/null +++ b/openproject/.env.example @@ -0,0 +1,20 @@ +## +# All environment variables defined here will only apply if you pass them +# to the OpenProject container in docker-compose.yml under x-op-app -> environment. +# For the examples here this is already the case. +# +# Please refer to our documentation to see all possible variables: +# https://www.openproject.org/docs/installation-and-operations/configuration/environment/ +# +OPENPROJECT_DOMAIN= +OPENPROJECT_TAG= +OPENPROJECT_HTTPS= +OPENPROJECT_HOST__NAME= +OPENPROJECT_PORT= +OPENPROJECT_IMAP_ENABLED=false +OPENPROJECT_POSTGRES_USER= +OPENPROJECT_POSTGRES_PASSWORD= +OPENPROJECT_POSTGRES_DB= +OPENPROJECT_RAILS_MIN_THREADS= +OPENPROJECT_RAILS_MAX_THREADS= +OPENPROJECT_OPDATA= diff --git a/openproject/.gitignore b/openproject/.gitignore new file mode 100644 index 0000000..d4284de --- /dev/null +++ b/openproject/.gitignore @@ -0,0 +1,4 @@ +.env + +docker-compose.override.yml +backups/ diff --git a/openproject/README.md b/openproject/README.md new file mode 100644 index 0000000..d6cc71a --- /dev/null +++ b/openproject/README.md @@ -0,0 +1,223 @@ +# OpenProject installation with Docker Compose + +This repository contains the installation method for OpenProject using Docker Compose. + + +> [!NOTE] +> Looking for the Kubernetes installation method? +> Please use the [OpenProject helm chart](https://charts.openproject.org) to install OpenProject on kubernetes. + +## Quick start + +First, you must clone the [openproject-deploy](https://github.com/opf/openproject-deploy/tree/stable/15/compose) repository: + +```shell +git clone https://github.com/opf/openproject-deploy --depth=1 --branch=stable/15 openproject +``` + +Copy the example `.env` file and edit any values you want to change: + +```shell +cp .env.example .env +vim .env +``` + +If you are using the default value of OPDATA that is used in the ```.env.example``` you need to make sure that the folder exist, and you have the right permissions: + +```shell +sudo mkdir -p /var/openproject/assets +sudo chown 1000:1000 -R /var/openproject/assets +``` + +Next you start up the containers in the background while making sure to pull the latest versions of all used images. + +```shell +OPENPROJECT_HTTPS=false docker compose up -d --build --pull always +``` + +After a while, OpenProject should be up and running on `http://localhost:8080`. The default username and password is login: `admin`, and password: `admin`. +The `OPENPROJECT_HTTPS=false` environment variable explicitly disables HTTPS mode for the first startup. Without this, OpenProject assumes it's running behind HTTPS in production by default. +We do strongly recommend you use OpenProject behind a TLS terminated proxy for production purposes and remove this flag before actually starting to use it. + +### Customization + +The `docker-compose.yml` file present in the repository can be adjusted to your convenience. But note that with each pull, it will be overwritten. +Best practice is to use the file `docker-compose.override.yml` for that case. +For instance you could mount specific configuration files, override environment variables, or switch off services you don't need. + +Please refer to the official [Docker Compose documentation](https://docs.docker.com/compose/extends/) for more details. + +### Troubleshooting + +**pull access denied for openproject/proxy, repository does not exist or may require 'docker login': denied: requested access to the resource is denied** + +If you encounter this after `docker compose up` this is merely a warning which can be ignored. + +If this happens during `docker compose pull` this is simply a warning as well. +But it will result in the command's exit code to be a failure even though all images are pulled. +To prevent this you can add the `--ignore-buildable` option, running `docker compose pull --ignore-buildable`. + +### HTTPS/SSL + +By default OpenProject starts with the HTTPS option **enabled**, but it **does not** handle SSL termination itself. This +is usually done separately via a [reverse proxy +setup](https://www.openproject.org/docs/installation-and-operations/installation/docker/#apache-reverse-proxy-setup). +Without this you will run into an `ERR_SSL_PROTOCOL_ERROR` when accessing OpenProject. + +See below how to disable HTTPS. + +Be aware that if you want to use the integrated Caddy proxy as a proxy with outbound connections, you need to rewrite the +`Caddyfile`. In the default state, it is configured to forward the `X-Forwarded-*` headers from the reverse proxy in +front of it and not setting them itself. This is considered a security flaw and should instead be solved by configuring +`trusted_proxies` inside the `Caddyfile`. For more information read +the [Caddy documentation](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy). + +### PORT + +By default the port is bound to `0.0.0.0` means access to OpenProject will be public. +See below how to change that. + +## Image configuration + +OpenProject publishes `slim` containers that you should be using for this compose setup. +Please see https://www.openproject.org/docs/installation-and-operations/installation/docker/#available-containers for more information on the containers and versions we push. + +## Configuration + +Environment variables can be added to `docker-compose.yml` under `x-op-app -> environment` to change +OpenProject's configuration. Some are already defined and can be changed via the environment. + +You can pass those variables directly when starting the stack as follows. + +``` +VARIABLE=value docker-compose up -d +``` + +You can also put those variables into an `.env` file in your current working +directory, and Docker Compose will pick it up automatically. See `.env.example` +for details. + +## HTTPS + +You can disable OpenProject's HTTPS option via: + +``` +OPENPROJECT_HTTPS=false +``` + +## PORT + +If you want to specify a different port, you can do so with: + +``` +PORT=4000 +``` + +If you don't want OpenProject to bind to `0.0.0.0` you can bind it to localhost only like this: + +``` +PORT=127.0.0.1:8080 +``` + +## TAG + +If you want to specify a custom tag for the OpenProject docker image, you can do so with: + +``` +TAG=my-docker-tag +``` + +## BIM edition + +In order to install or change to BIM inside a Docker environment, please navigate to the [Docker Installation for OpenProject BIM](https://www.openproject.org/docs/installation-and-operations/bim-edition/#docker-installation-openproject-bim) paragraph at the BIM edition documentation. + +## Upgrade + +Retrieve any changes from the `openproject-deploy` repository: + + git pull origin stable/15 + +Build the control plane: + + docker-compose -f docker-compose.yml -f docker-compose.control.yml build + +Take a backup of your existing postgresql data and openproject assets: + + docker-compose -f docker-compose.yml -f docker-compose.control.yml run backup + +Run the upgrade: + + docker-compose -f docker-compose.yml -f docker-compose.control.yml run upgrade + +Relaunch the containers, ensure you are pulling to use the latest version of the Docker images: + + docker compose up -d --build --pull always + + + +## Backup + +Switch off your current installation: + + docker-compose down + +Build the control scripts: + + docker-compose -f docker-compose.yml -f docker-compose.control.yml build + +Take a backup of your existing PostgreSQL data and OpenProject assets: + + docker-compose -f docker-compose.yml -f docker-compose.control.yml run backup + +Restart your OpenProject installation + + docker-compose up -d + + + +## Uninstall + +If you want to stop the containers without removing them directly: + +```bash +docker-compose stop +``` + +You can remove the container stack with: + +```bash +docker-compose down +``` + +> [!NOTE] +> This will not remove your data which is persisted in named volumes, likely called `compose_opdata` (for attachments) and `compose_pgdata` (for the database). +> The exact name depends on the name of the directory where your `docker-compose.yml` and/or you `docker-compose.override.yml` files are stored (`compose` in this case). + +If you want to start from scratch and remove the existing data you will have to remove these volumes via +`docker volume rm compose_opdata compose_pgdata`. + +## Troubleshooting + +You can look at the logs with: + + docker-compose logs -n 1000 + +For the complete documentation, please refer to https://docs.openproject.org/installation-and-operations/. + +### Network issues + +If you're running into weird network issues and timeouts such as the one described in +[OP#42802](https://community.openproject.org/work_packages/42802), you might have success in remove the two separate +frontend and backend networks. This might be connected to using podman for orchestration, although we haven't been able +to confirm this. + +### SMTP setup fails: Network is unreachable. + +Make sure your container has DNS resolution to access external SMTP server when set up as described in +[OP#44515](https://community.openproject.org/work_packages/44515). + +```yml +worker: + dns: + - "Your DNS IP" # OR add a public DNS resolver like 8.8.8.8 +``` diff --git a/openproject/control/Dockerfile b/openproject/control/Dockerfile new file mode 100644 index 0000000..a90a259 --- /dev/null +++ b/openproject/control/Dockerfile @@ -0,0 +1,11 @@ +FROM debian:12 + +RUN apt-get update -qq && apt-get install wget gnupg2 -y && rm -rf /var/lib/apt/lists/* +RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - +RUN echo "deb http://apt.postgresql.org/pub/repos/apt bookworm-pgdg main" > /etc/apt/sources.list.d/pgdg.list +RUN apt-get update -qq && apt-get install postgresql-9.6 postgresql-10 postgresql-13 -y && rm -rf /var/lib/apt/lists/* +RUN localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 + +ENV LANG en_US.utf8 + +ADD . /control diff --git a/openproject/control/backup/entrypoint.sh b/openproject/control/backup/entrypoint.sh new file mode 100755 index 0000000..f81abe2 --- /dev/null +++ b/openproject/control/backup/entrypoint.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -e + +timestamp=$(date +%s) +mkdir -p /backups +cd /backups +filename="${timestamp}-pgdata.tar.gz" +echo "Backing up PostgreSQL data into backups/${filename}..." +tar czf "${filename}" -C "$PGDATA" . +filename="${timestamp}-opdata.tar.gz" +echo "Backing up OpenProject assets into backups/${filename}..." +tar czf "${filename}" -C "$OPDATA" . +echo "DONE" diff --git a/openproject/control/upgrade/entrypoint.sh b/openproject/control/upgrade/entrypoint.sh new file mode 100755 index 0000000..9cbc947 --- /dev/null +++ b/openproject/control/upgrade/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e +set -o pipefail + +/control/upgrade/scripts/00-db-upgrade.sh + +echo "Please restart your installation by issuing the following command:" +echo " docker compose up -d --build --pull always" diff --git a/openproject/control/upgrade/scripts/00-db-upgrade.sh b/openproject/control/upgrade/scripts/00-db-upgrade.sh new file mode 100755 index 0000000..1e89bad --- /dev/null +++ b/openproject/control/upgrade/scripts/00-db-upgrade.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -e +set -o pipefail + +CURRENT_PGVERSION="$(cat $PGDATA/PG_VERSION)" +NEW_PGVERSION="13" +PGWORKDIR=${PGWORKDIR:=/var/lib/postgresql/work} + +if [ ! "$CURRENT_PGVERSION" -lt "$NEW_PGVERSION" ]; then + echo "Current PG version is higher or equal to the PG version to be installed ($CURRENT_PGVERSION > $NEW_PGVERSION). Ignoring." + exit 0 +fi + +export PGBINOLD="/usr/lib/postgresql/$CURRENT_PGVERSION/bin" +export PGBINNEW="/usr/lib/postgresql/$NEW_PGVERSION/bin" +export PGDATAOLD="$PGDATA" +export PGDATANEW="$PGWORKDIR/datanew" + +rm -rf "$PGWORKDIR" && mkdir -p "$PGWORKDIR" "$PGDATANEW" +chown -R postgres.postgres "$PGDATA" "$PGWORKDIR" +cd "$PGWORKDIR" +# initialize new db +su -m postgres -c "$PGBINNEW/initdb --pgdata=$PGDATANEW --encoding=unicode --auth=trust" +echo "Performing a dry-run migration to PostgreSQL $NEW_PGVERSION..." +su -m postgres -c "$PGBINNEW/pg_upgrade -c" +echo "Performing the real migration to PostgreSQL $NEW_VERSION..." +su -m postgres -c "$PGBINNEW/pg_upgrade" +su -m postgres -c "rm -rf $PGDATAOLD/* && mv $PGDATANEW/* $PGDATAOLD/" +# as per docker hub documentation +su -m postgres -c "echo \"listen_addresses = '*'\" >> $PGDATAOLD/postgresql.conf" +su -m postgres -c "echo \"host all all all md5\" >> $PGDATAOLD/pg_hba.conf" +echo "DONE" diff --git a/openproject/create_infra.bash b/openproject/create_infra.bash new file mode 100755 index 0000000..e3cebe6 --- /dev/null +++ b/openproject/create_infra.bash @@ -0,0 +1,8 @@ +#!/bin/bash + +source .env +source ../core/.env + +docker exec postgres psql -U $POSTGRES_USER -d postgres -c "CREATE USER $OPENPROJECT_DB_USER WITH PASSWORD '$OPENPROJECT_DB_PASSWORD';" +docker exec postgres psql -U $POSTGRES_USER -d postgres -c "CREATE DATABASE $OPENPROJECT_DB_NAME OWNER $OPENPROJECT_DB_USER;" +docker exec postgres psql -U $POSTGRES_USER -d $OPENPROJECT_DB_NAME -c "GRANT ALL PRIVILEGES ON DATABASE $OPENPROJECT_DB_NAME TO $OPENPROJECT_DB_USER;" diff --git a/openproject/docker-compose.control.yml b/openproject/docker-compose.control.yml new file mode 100644 index 0000000..947efef --- /dev/null +++ b/openproject/docker-compose.control.yml @@ -0,0 +1,50 @@ +version: "3.7" + +volumes: + pgupgrade: + +services: + db: + restart: "no" + entrypoint: ["echo", "disabled"] + upgrade: + restart: "no" + build: + context: ./control + environment: + PGDATA: /var/lib/postgresql/data + volumes: + - "${OPENPROJECT_PGDATA:-pgdata}:/var/lib/postgresql/data" + - "./control:/control" + entrypoint: ["/control/upgrade/entrypoint.sh"] + backup: + restart: "no" + build: + context: ./control + environment: + PGDATA: /var/lib/postgresql/data + OPDATA: /var/openproject/assets + volumes: + - "${OPENPROJECT_OPDATA:-opdata}:/var/openproject/assets" + - "./backups:/backups" + - "./control:/control" + entrypoint: ["/control/backup/entrypoint.sh"] + web: + restart: "no" + entrypoint: ["echo", "disabled"] + worker: + restart: "no" + entrypoint: ["echo", "disabled"] + cron: + restart: "no" + entrypoint: ["echo", "disabled"] + seeder: + restart: "no" + entrypoint: ["echo", "disabled"] + proxy: + restart: "no" + entrypoint: ["echo", "disabled"] + cache: + restart: "no" + entrypoint: ["echo", "disabled"] + diff --git a/openproject/docker-compose.yml b/openproject/docker-compose.yml new file mode 100644 index 0000000..304af67 --- /dev/null +++ b/openproject/docker-compose.yml @@ -0,0 +1,117 @@ +networks: + database: + external: true + frontend: + backend: + traefik: + external: true +volumes: + opdata: + +x-op-restart-policy: &restart_policy + restart: unless-stopped +x-op-image: &image + image: openproject/openproject:${OPENPROJECT_TAG:-15-slim} +x-op-app: &app + <<: [*image, *restart_policy] + environment: + OPENPROJECT_HTTPS: "${OPENPROJECT_HTTPS:-true}" + OPENPROJECT_HOST__NAME: "${OPENPROJECT_HOST__NAME:-localhost:8080}" + OPENPROJECT_HSTS: "${OPENPROJECT_HSTS:-true}" + RAILS_CACHE_STORE: "memcache" + OPENPROJECT_CACHE__MEMCACHE__SERVER: "cache:11211" + OPENPROJECT_RAILS__RELATIVE__URL__ROOT: "${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}" + DATABASE_URL: "postgres://${OPENPROJECT_DB_USER:-openproject}:${OPENPROJECT_DB_PASSWORD:-openproject}@postgres/openproject?pool=20&encoding=unicode&reconnect=true" + RAILS_MIN_THREADS: ${OPENPROJECT_RAILS_MIN_THREADS:-4} + RAILS_MAX_THREADS: ${OPENPROJECT_RAILS_MAX_THREADS:-16} + # set to true to enable the email receiving feature. See ./docker/cron for more options + IMAP_ENABLED: "${OPENPROJECT_IMAP_ENABLED:-false}" + volumes: + - "${OPENPROJECT_OPDATA:-opdata}:/var/openproject/assets" + +services: + cache: + image: memcached + networks: + - backend + - database + restart: unless-stopped + + proxy: + build: + context: ./proxy + args: + APP_HOST: web + image: openproject/proxy + depends_on: + - web + labels: + - traefik.enable=true + - traefik.docker.network=traefik + - traefik.http.routers.openproject.entrypoints=web,websecure + - traefik.http.routers.openproject.middlewares=https-redirect + - traefik.http.routers.openproject.tls=true + - traefik.http.routers.openproject.tls.certresolver=le + - traefik.http.routers.openproject.rule=Host(`${OPENPROJECT_DOMAIN}`) + - traefik.http.services.openproject.loadbalancer.server.port=80 + networks: + - traefik + - frontend + restart: unless-stopped + + web: + <<: *app + command: "./docker/prod/web" + networks: + - database + - frontend + - backend + depends_on: + - cache + - seeder + labels: + - autoheal=true + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080${OPENPROJECT_RAILS__RELATIVE__URL__ROOT:-}/health_checks/default"] + interval: 10s + timeout: 3s + retries: 3 + start_period: 30s + + autoheal: + image: willfarrell/autoheal:1.2.0 + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" + environment: + AUTOHEAL_CONTAINER_LABEL: autoheal + AUTOHEAL_START_PERIOD: 600 + AUTOHEAL_INTERVAL: 30 + + worker: + <<: *app + command: "./docker/prod/worker" + networks: + - backend + - database + depends_on: + - cache + - seeder + + cron: + <<: *app + command: "./docker/prod/cron" + networks: + - backend + - database + depends_on: + - cache + - seeder + + seeder: + <<: *app + command: "./docker/prod/seeder" + restart: on-failure + networks: + - backend + - database + diff --git a/openproject/proxy/Caddyfile.template b/openproject/proxy/Caddyfile.template new file mode 100644 index 0000000..6cc4730 --- /dev/null +++ b/openproject/proxy/Caddyfile.template @@ -0,0 +1,26 @@ +{ + # Global options + servers { + # Configure trusted proxies to correctly handle forwarded headers from Traefik + trusted_proxies static private_ranges + } +} + +:80 { + reverse_proxy * http://${APP_HOST}:8080 { + # The following directives are needed to make the proxy forward explicitly the X-Forwarded-* headers. If unset, + # Caddy will reset them. See: https://caddyserver.com/docs/caddyfile/directives/reverse_proxy#defaults + # This is needed, if you are using a reverse proxy in front of the compose stack and Caddy is NOT your first + # point of contact. + # When using Caddy is reachable as a first point of contact, it is highly recommended to configure the server's + # global `trusted_proxies` directive. See: https://caddyserver.com/docs/caddyfile/options#trusted-proxies + + header_up X-Forwarded-Proto {header.X-Forwarded-Proto} + header_up X-Forwarded-For {header.X-Forwarded-For} + header_up X-Forwarded-Host {header.X-Forwarded-Host} + } + + file_server + + log +} diff --git a/openproject/proxy/Dockerfile b/openproject/proxy/Dockerfile new file mode 100644 index 0000000..5a6591b --- /dev/null +++ b/openproject/proxy/Dockerfile @@ -0,0 +1,8 @@ +FROM caddy:2 + +COPY ./Caddyfile.template /etc/caddy/Caddyfile.template + +ARG APP_HOST +RUN sed 's|${APP_HOST}|'"$APP_HOST"'|g' /etc/caddy/Caddyfile.template > /etc/caddy/Caddyfile + +ENTRYPOINT ["caddy", "run", "--config", "/etc/caddy/Caddyfile"]