commit 97cfcefe2276b2224fe7debb2dee9b6cb0dcf4db Author: data Date: Tue Jan 27 18:16:40 2026 +0100 Neues Modul epcqr verbessert diff --git a/COPYING b/COPYING new file mode 100755 index 0000000..94a0453 --- /dev/null +++ b/COPYING @@ -0,0 +1,621 @@ + 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 diff --git a/ChangeLog.md b/ChangeLog.md new file mode 100755 index 0000000..b41ca72 --- /dev/null +++ b/ChangeLog.md @@ -0,0 +1,47 @@ +# CHANGELOG MODULE EPCQR FOR [DOLIBARR ERP CRM](https://www.dolibarr.org) + +## 1.5 (2026-01-27) + +### Neue Features +- ✅ Lokale QR-Code-Generierung mit Caching-System +- ✅ `{qrcode}` Keyword für ODT-Templates +- ✅ Generisches Bildintegration-System für beliebige Bilder +- ✅ Substitutionssystem aktiviert und implementiert +- ✅ Hook-basierte ODT-Verarbeitung (afterODTCreation) +- ✅ Automatische Bildgrößen-Anpassung in ODT + +### Neue Dateien +- `lib/qrcode.class.php` - QR-Code Generator mit Caching +- `core/substitutions/functions_epcqr.lib.php` - Substitutionsfunktionen +- `class/actions_epcqr.class.php` - Hook-Klasse für ODT-Verarbeitung +- `doc/BILDER_IN_ODT.md` - Ausführliche Dokumentation +- `sql/update_1.5.0.sql` - SQL-Update für neue Extrafelder + +### Erweiterte Funktionen +- `lib/epcqr.lib.php` - Neue Hilfsfunktionen: + - `epcqr_generateQRCodeForInvoice()` - QR-Code für Rechnung generieren + - `epcqr_processODTImages()` - Bilder in ODT einfügen + - `epcqr_insertImagesIntoODT()` - ODT-ZIP-Verarbeitung + +### Änderungen +- Trigger `BILL_VALIDATE` nutzt nun lokale QR-Code-Generierung +- Modul-Konfiguration: `substitutions` aktiviert (1) +- Modul-Konfiguration: Hooks für `pdfgeneration`, `odtgeneration` aktiviert +- Versionsnummer erhöht auf 1.5 + +### Kompatibilität +- Bestehende Extrafelder bleiben erhalten (qrcode, qrcodepfad) +- Neues Extrafeld: qrcodepath (lokaler Pfad) +- Alte ODT-Templates funktionieren weiterhin + +## 1.4 (2026-01-11) + +Initial stable version +- Automatische QR-Code Generierung bei BILL_VALIDATE +- Extra-Felder für ODT-Integration +- Externe QR-Service Integration +- Object-Reload Fix für finale Rechnungsnummer + +## 1.3 + +Initial version diff --git a/README.md b/README.md new file mode 100755 index 0000000..8657955 --- /dev/null +++ b/README.md @@ -0,0 +1,478 @@ +# EPCQR - QR-Code Generator für Dolibarr Rechnungen + +**Version:** 1.5 +**Autor:** Eduard Wisch +**Dolibarr:** 13.x - 20.x +**Lizenz:** GPL-3.0+ + +--- + +## Übersicht + +Das **EPCQR Modul** generiert automatisch **EPC-QR-Codes** (European Payments Council) für Dolibarr-Rechnungen und bietet eine **wiederverwendbare Lösung** zum Einfügen von Bildern in ODT-Dokumentvorlagen. + +### Neu in Version 1.5 + +✅ **Lokale QR-Code-Generierung** mit Caching +✅ **Generisches Bildintegration-System** für ODT-Dokumente +✅ **{qrcode} Keyword** in ODT-Templates +✅ **Substitutionssystem** für beliebige Bilder +✅ **Hook-basierte ODT-Verarbeitung** + +### Was sind EPC-QR-Codes? + +EPC-QR-Codes enthalten alle relevanten Zahlungsinformationen im GiroCode-Format: +- Empfänger (Kontoinhaber) +- IBAN +- BIC +- Betrag +- Verwendungszweck (Rechnungsnummer) + +--- + +## Features + +### QR-Code Generierung +✅ **Automatische Generierung** bei Rechnungsfreigabe +✅ **Lokales Caching** (keine externe Service-Abhängigkeit mehr) +✅ **EPC/GiroCode-Standard** konform +✅ **Finale Rechnungsnummer** im QR-Code (keine PROV-Nummern) + +### Bildintegration in ODT +✅ **{qrcode} Keyword** für einfache Verwendung in Templates +✅ **Generisches System** für beliebige Bilder +✅ **Automatische Bildgrößen-Anpassung** +✅ **Wiederverwendbar** für andere Module + +### Technisch +✅ **Hook-System** für ODT-Verarbeitung +✅ **Substitutionsfunktionen** für Dokumente +✅ **Extra-Felder** mit `_imagepath` Suffix automatisch erkannt +✅ **Vollständige ODT-ZIP-Verarbeitung** + +--- + +## Installation + +### 1. Modul aktivieren + +1. Gehe zu: **Home → Setup → Modules/Applications** +2. Suche nach "EPCQR" +3. Klicke auf **Activate** + +### 2. Extra-Felder werden automatisch erstellt + +Das Modul erstellt automatisch 3 Extra-Felder für Rechnungen: + +| Feldname | Typ | Beschreibung | +|----------|-----|--------------| +| `qrcode` | HTML | QR-Code als `` Tag (Kompatibilität) | +| `qrcodepfad` | Varchar(255) | QR-Code URL via viewimage.php | +| `qrcodepath` | Varchar(255) | **NEU**: Lokaler Dateipfad für ODT-Integration | + +--- + +## Konfiguration + +### Bankdaten anpassen + +**WICHTIG:** Du musst deine Bankdaten im Code hinterlegen! + +**Datei:** `/custom/epcqr/class/actions_epcqr.class.php` + +Suche nach dieser Stelle im Trigger `BILL_VALIDATE`: + +```php +// Bankdaten - HIER ANPASSEN! +$accountHolder = 'Peter Casimir'; +$iban = 'DE70217625500013438147'; +$bic = 'GENODEF1HUM'; +``` + +**Ändere diese Werte auf deine Daten:** + +```php +$accountHolder = 'Dein Name / Firmenname'; +$iban = 'DE12345678901234567890'; +$bic = 'ABCDEFGH123'; +``` + +**Speichern und fertig!** + +--- + +## Verwendung + +### Automatische QR-Code Generierung + +Der QR-Code wird **automatisch** generiert wenn du eine Rechnung freigibst: + +1. Rechnung erstellen (Status: Draft) +2. **Freigeben** (Validate) +3. QR-Code wird automatisch generiert +4. Extra-Felder werden befüllt + +### QR-Code in ODT-Templates einfügen + +**NEU in Version 1.5: Verwenden Sie das einfache `{qrcode}` Keyword!** + +**Schritt 1:** ODT-Template öffnen (z.B. in LibreOffice) + +**Schritt 2:** An gewünschter Stelle einfügen: + +``` +{qrcode} +``` + +**Schritt 3:** Template speichern und hochladen + +**Schritt 4:** Rechnung freigeben → QR-Code wird automatisch generiert + +**Schritt 5:** Dokument als ODT generieren → QR-Code wird eingefügt + +**Ergebnis:** Der QR-Code erscheint automatisch als Bild im ODT-Dokument! + +**Hinweis:** Das alte Format `{invoice_options_qrcode}` funktioniert weiterhin für Kompatibilität. + +### Beispiel Template-Position + +``` +┌─────────────────────────────────┐ +│ RECHNUNG IN2601-0001 │ +│ │ +│ Gesamtbetrag: 1.234,56 € │ +│ │ +│ {invoice_options_qrcode} │ ← QR-Code hier +│ │ +│ Bitte überweisen Sie... │ +└─────────────────────────────────┘ +``` + +--- + +## Technische Details + +### Wie funktioniert das Modul? + +#### 1. Trigger-System + +Das Modul nutzt den Dolibarr-Trigger **BILL_VALIDATE**, der bei Rechnungsfreigabe ausgeführt wird. + +**Problem:** Zu diesem Zeitpunkt hat das `$object` noch die provisorische Nummer (z.B. `PROV23`)! + +**Lösung:** Das Modul lädt das Rechnungsobjekt neu aus der Datenbank: + +```php +$invoice = new Facture($this->db); +$invoice->fetch($object->id); +// Jetzt hat $invoice->ref die finale Nummer (z.B. IN26-0001) +``` + +#### 2. QR-Code Service + +Das Modul nutzt einen externen Service: + +**URL:** `https://qr.data-it-solution.de/epc` + +**Parameter:** +- `name` = Kontoinhaber +- `iban` = IBAN +- `bic` = BIC/SWIFT +- `amount` = Betrag (formatiert mit `price2num()`) +- `remittance` = Verwendungszweck (Rechnungsnummer) + +**Beispiel-URL:** +``` +https://qr.data-it-solution.de/epc?name=Eduard+Wisch&iban=DE70217625500013438147&bic=GENODEF1HUM&amount=1234.56&remittance=IN26-0001 +``` + +#### 3. Speicherung + +Der QR-Code wird in 2 Extra-Feldern gespeichert: + +```php +// Als HTML-Tag für ODT +$invoice->array_options['options_qrcode'] = ""; + +// Als URL für Backup/Referenz +$invoice->array_options['options_qrcodepfad'] = $qrurl; +``` + +--- + +## Workflow-Diagramm + +``` +┌─────────────────────┐ +│ Rechnung erstellen │ +│ (Status: DRAFT) │ +└──────────┬──────────┘ + │ + ▼ +┌─────────────────────┐ +│ Freigeben (Validate)│ +└──────────┬──────────┘ + │ + ▼ +┌─────────────────────────────┐ +│ 1. Datenbank Update │ +│ → Finale Nummer vergeben │ +└──────────┬──────────────────┘ + │ + ▼ +┌─────────────────────────────┐ +│ 2. Trigger BILL_VALIDATE │ +│ → Modul wird aufgerufen │ +└──────────┬──────────────────┘ + │ + ▼ +┌─────────────────────────────┐ +│ 3. Object neu laden │ +│ → Finale Nummer holen │ +└──────────┬──────────────────┘ + │ + ▼ +┌─────────────────────────────┐ +│ 4. QR-URL generieren │ +│ → Mit finaler Nummer │ +└──────────┬──────────────────┘ + │ + ▼ +┌─────────────────────────────┐ +│ 5. Extra-Felder speichern │ +│ → options_qrcode │ +│ → options_qrcodepfad │ +└─────────────────────────────┘ +``` + +--- + +## Troubleshooting + +### Problem: QR-Code zeigt PROV-Nummer + +**Symptom:** QR-Code enthält "PROV23" statt "IN26-0001" + +**Ursache:** Object wurde nicht neu geladen im Trigger + +**Lösung:** Prüfe in `actions_epcqr.class.php`: + +```php +// FALSCH: +$ref = $object->ref; + +// RICHTIG: +$invoice = new Facture($this->db); +$invoice->fetch($object->id); +$ref = $invoice->ref; +``` + +### Problem: Extra-Felder sind leer + +**Symptom:** Nach Freigabe sind die Felder `qrcode` und `qrcodepfad` leer + +**Lösung:** +1. Modul **deaktivieren** +2. Modul wieder **aktivieren** +3. Prüfe: **Setup → Dictionaries → Extra Attributes → Invoices** +4. Sollten 2 Felder da sein: `qrcode` und `qrcodepfad` + +### Problem: QR-Code wird in ODT nicht angezeigt + +**Symptom:** Template zeigt nur `{invoice_options_qrcode}` als Text + +**Lösungen:** +1. **Feldname prüfen:** Muss exakt `{invoice_options_qrcode}` heißen +2. **Extra-Feld aktiviert?** Setup → Extra Attributes → "Printable" = Ja +3. **Template neu generieren:** Rechnung → "Generate document" + +### Problem: Falscher Betrag im QR-Code + +**Symptom:** Banking-App zeigt falschen Betrag + +**Ursache:** Formatierung falsch + +**Lösung:** Modul nutzt korrekt `price2num($invoice->total_ttc, 'MT')` + +### Problem: QR-Code kann nicht gescannt werden + +**Mögliche Ursachen:** +- QR-Code zu klein im PDF → Größe in ODT anpassen +- Externe URL nicht erreichbar → Internetverbindung prüfen +- Falsches Format → Service-URL prüfen + +--- + +## Extra-Felder Details + +### Feld: qrcode + +- **Typ:** HTML +- **Inhalt:** `` +- **Verwendung:** Für ODT-Templates +- **Printable:** Ja +- **Position:** 100 + +### Feld: qrcodepfad + +- **Typ:** varchar(255) +- **Inhalt:** Vollständige QR-Code URL +- **Verwendung:** Backup, Debugging, externe Verarbeitung +- **Printable:** Nein (nur intern) +- **Position:** 101 + +--- + +## Code-Referenz + +### Kompletter Trigger-Code + +```php +public function runTrigger($action, $object, User $user, Translate $langs, Conf $conf) +{ + if ($action == 'BILL_VALIDATE') { + + dol_syslog("EPCQR: Generiere QR-Code für Rechnung ID " . $object->id); + + // ⚠️ WICHTIG: Objekt neu laden für finale Rechnungsnummer! + require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php'; + $invoice = new Facture($this->db); + $result = $invoice->fetch($object->id); + + if ($result > 0) { + // Bankdaten - HIER ANPASSEN! + $accountHolder = 'Eduard Wisch'; + $iban = 'DE70217625500013438147'; + $bic = 'GENODEF1HUM'; + $amount = price2num($invoice->total_ttc, 'MT'); + $ref = $invoice->ref; // Finale Nummer + + // QR-Code URL generieren + $qrurl = "https://qr.data-it-solution.de/epc?" + . "name=" . urlencode($accountHolder) + . "&iban=" . urlencode($iban) + . "&bic=" . urlencode($bic) + . "&amount=" . urlencode($amount) + . "&remittance=" . urlencode($ref); + + // In Extra-Felder speichern + $invoice->array_options['options_qrcode'] = ""; + $invoice->insertExtraFields(); + $invoice->array_options['options_qrcodepfad'] = $qrurl; + $invoice->insertExtraFields(); + + dol_syslog("EPCQR: QR-Code generiert für " . $invoice->ref); + } else { + dol_syslog("EPCQR: Fehler beim Laden der Rechnung", LOG_ERR); + return -1; + } + + return 1; + } + + return 0; +} +``` + +--- + +## FAQ + +### Kann ich andere QR-Services nutzen? + +Ja! Ändere einfach die URL in `actions_epcqr.class.php`: + +```php +$qrurl = "https://dein-service.de/qr?" + . "deine_parameter=" . urlencode($value); +``` + +### Funktioniert das Modul mit allen Rechnungstypen? + +Ja, der Trigger `BILL_VALIDATE` wird für alle Rechnungstypen aufgerufen: +- Standard-Rechnungen +- Gutschriften +- Ersatzrechnungen +- Anzahlungsrechnungen + +### Kann ich die QR-Code-Größe ändern? + +Ja, in 2 Varianten: + +**1. Im Code:** +```php +$invoice->array_options['options_qrcode'] = ""; +``` + +**2. Im ODT-Template:** +Bild markieren → Rechtsklick → Eigenschaften → Größe ändern + +### Werden alte Rechnungen auch mit QR-Codes versehen? + +Nein, nur neue Rechnungen die nach Modulaktivierung **freigegeben** werden. + +**Für alte Rechnungen:** +1. Rechnung auf "Draft" setzen +2. Erneut freigeben +3. QR-Code wird generiert + +--- + +## Dokumentation + +📖 **Ausführliche Dokumentation:** [doc/BILDER_IN_ODT.md](doc/BILDER_IN_ODT.md) + +Die erweiterte Dokumentation enthält: +- Technische Details zur Bildverarbeitung +- Anleitung für eigene Bilder +- Fehlerbehebung und Debugging +- Code-Referenz und Architektur +- Erweiterte Verwendungsmöglichkeiten + +--- + +## Support & Kontakt + +**Entwickler:** Eduard Wisch +**E-Mail:** data@data-it-solution.de + +### Bei Problemen + +1. **Log-Dateien prüfen:** `documents/dolibarr.log` +2. **Debug aktivieren:** Setup → Other → Enable debug mode +3. **Dokumentation lesen:** [doc/BILDER_IN_ODT.md](doc/BILDER_IN_ODT.md) + +--- + +## Changelog + +### Version 1.5 (2026-01-27) +- ✅ **Lokale QR-Code-Generierung** mit Caching-System +- ✅ **{qrcode} Keyword** für ODT-Templates +- ✅ **Generisches Bildintegration-System** für beliebige Bilder +- ✅ **Substitutionssystem** aktiviert und implementiert +- ✅ **Hook-basierte ODT-Verarbeitung** (afterODTCreation) +- ✅ **Automatische Bildgrößen-Anpassung** in ODT +- ✅ **Wiederverwendbare Lösung** für andere Module +- ✅ Neue Klassen: QRCodeGenerator, ActionsEpcqr +- ✅ Neue Funktionen: epcqr_processODTImages, epcqr_insertImagesIntoODT +- ✅ Dokumentation: [doc/BILDER_IN_ODT.md](doc/BILDER_IN_ODT.md) + +### Version 1.4 (2026-01-11) +- ✅ Erste stabile Version +- ✅ Automatische QR-Code Generierung +- ✅ Extra-Felder für ODT-Integration +- ✅ Externe QR-Service Integration +- ✅ Object-Reload Fix für finale Rechnungsnummer + +--- + +## Lizenz + +GPL-3.0+ + +Dieses Modul ist freie Software; Sie können es unter den Bedingungen der GNU General Public License Version 3 oder später weitergeben und/oder modifizieren. + +--- + +**Viel Erfolg mit EPCQR!** 🎉 diff --git a/README.md.back b/README.md.back new file mode 100755 index 0000000..8292deb --- /dev/null +++ b/README.md.back @@ -0,0 +1,426 @@ +# EPCQR - QR-Code Generator für Dolibarr Rechnungen + +**Version:** 1.4 +**Autor:** Eduard Wisch +**Dolibarr:** 13.x - 20.x +**Lizenz:** GPL-3.0+ + +--- + +## Übersicht + +Das **EPCQR Modul** generiert automatisch **EPC-QR-Codes** (European Payments Council) für Dolibarr-Rechnungen. Diese QR-Codes ermöglichen es Kunden, Rechnungen direkt mit ihrer Banking-App zu scannen und zu bezahlen - ohne manuelle Eingabe von IBAN, Betrag oder Verwendungszweck. + +### Was sind EPC-QR-Codes? + +EPC-QR-Codes enthalten alle relevanten Zahlungsinformationen im GiroCode-Format: +- Empfänger (Kontoinhaber) +- IBAN +- BIC +- Betrag +- Verwendungszweck (Rechnungsnummer) + +--- + +## Features + +✅ **Automatische Generierung** bei Rechnungsfreigabe +✅ **EPC/GiroCode-Standard** konform +✅ **Externe QR-Service Integration** (kein lokaler PHP-Code nötig) +✅ **Extra-Felder** für ODT-Template Integration +✅ **Finale Rechnungsnummer** im QR-Code (keine PROV-Nummern) +✅ **Einfache Konfiguration** über Code + +--- + +## Installation + +### 1. Modul aktivieren + +1. Gehe zu: **Home → Setup → Modules/Applications** +2. Suche nach "EPCQR" +3. Klicke auf **Activate** + +### 2. Extra-Felder werden automatisch erstellt + +Das Modul erstellt automatisch 2 Extra-Felder für Rechnungen: + +| Feldname | Typ | Beschreibung | +|----------|-----|--------------| +| `qrcode` | HTML | QR-Code als `` Tag für ODT-Templates | +| `qrcodepfad` | Varchar(255) | Vollständige QR-Code URL (Backup) | + +--- + +## Konfiguration + +### Bankdaten anpassen + +**WICHTIG:** Du musst deine Bankdaten im Code hinterlegen! + +**Datei:** `/custom/epcqr/class/actions_epcqr.class.php` + +Suche nach dieser Stelle im Trigger `BILL_VALIDATE`: + +```php +// Bankdaten - HIER ANPASSEN! +$accountHolder = 'Eduard Wisch'; +$iban = 'DE70217625500013438147'; +$bic = 'GENODEF1HUM'; +``` + +**Ändere diese Werte auf deine Daten:** + +```php +$accountHolder = 'Dein Name / Firmenname'; +$iban = 'DE12345678901234567890'; +$bic = 'ABCDEFGH123'; +``` + +**Speichern und fertig!** + +--- + +## Verwendung + +### Automatische QR-Code Generierung + +Der QR-Code wird **automatisch** generiert wenn du eine Rechnung freigibst: + +1. Rechnung erstellen (Status: Draft) +2. **Freigeben** (Validate) +3. QR-Code wird automatisch generiert +4. Extra-Felder werden befüllt + +### QR-Code in ODT-Templates einfügen + +**Schritt 1:** ODT-Template öffnen (z.B. in LibreOffice) + +**Schritt 2:** An gewünschter Stelle einfügen: + +``` +{invoice_options_qrcode} +``` + +**Schritt 3:** Template speichern und hochladen + +**Ergebnis:** Der QR-Code erscheint automatisch auf jeder generierten Rechnung! + +### Beispiel Template-Position + +``` +┌─────────────────────────────────┐ +│ RECHNUNG IN26-0001 │ +│ │ +│ Gesamtbetrag: 1.234,56 € │ +│ │ +│ {invoice_options_qrcode} │ ← QR-Code hier +│ │ +│ Bitte überweisen Sie... │ +└─────────────────────────────────┘ +``` + +--- + +## Technische Details + +### Wie funktioniert das Modul? + +#### 1. Trigger-System + +Das Modul nutzt den Dolibarr-Trigger **BILL_VALIDATE**, der bei Rechnungsfreigabe ausgeführt wird. + +**Problem:** Zu diesem Zeitpunkt hat das `$object` noch die provisorische Nummer (z.B. `PROV23`)! + +**Lösung:** Das Modul lädt das Rechnungsobjekt neu aus der Datenbank: + +```php +$invoice = new Facture($this->db); +$invoice->fetch($object->id); +// Jetzt hat $invoice->ref die finale Nummer (z.B. IN26-0001) +``` + +#### 2. QR-Code Service + +Das Modul nutzt einen externen Service: + +**URL:** `https://qr.data-it-solution.de/epc` + +**Parameter:** +- `name` = Kontoinhaber +- `iban` = IBAN +- `bic` = BIC/SWIFT +- `amount` = Betrag (formatiert mit `price2num()`) +- `remittance` = Verwendungszweck (Rechnungsnummer) + +**Beispiel-URL:** +``` +https://qr.data-it-solution.de/epc?name=Eduard+Wisch&iban=DE70217625500013438147&bic=GENODEF1HUM&amount=1234.56&remittance=IN26-0001 +``` + +#### 3. Speicherung + +Der QR-Code wird in 2 Extra-Feldern gespeichert: + +```php +// Als HTML-Tag für ODT +$invoice->array_options['options_qrcode'] = ""; + +// Als URL für Backup/Referenz +$invoice->array_options['options_qrcodepfad'] = $qrurl; +``` + +--- + +## Workflow-Diagramm + +``` +┌─────────────────────┐ +│ Rechnung erstellen │ +│ (Status: DRAFT) │ +└──────────┬──────────┘ + │ + ▼ +┌─────────────────────┐ +│ Freigeben (Validate)│ +└──────────┬──────────┘ + │ + ▼ +┌─────────────────────────────┐ +│ 1. Datenbank Update │ +│ → Finale Nummer vergeben │ +└──────────┬──────────────────┘ + │ + ▼ +┌─────────────────────────────┐ +│ 2. Trigger BILL_VALIDATE │ +│ → Modul wird aufgerufen │ +└──────────┬──────────────────┘ + │ + ▼ +┌─────────────────────────────┐ +│ 3. Object neu laden │ +│ → Finale Nummer holen │ +└──────────┬──────────────────┘ + │ + ▼ +┌─────────────────────────────┐ +│ 4. QR-URL generieren │ +│ → Mit finaler Nummer │ +└──────────┬──────────────────┘ + │ + ▼ +┌─────────────────────────────┐ +│ 5. Extra-Felder speichern │ +│ → options_qrcode │ +│ → options_qrcodepfad │ +└─────────────────────────────┘ +``` + +--- + +## Troubleshooting + +### Problem: QR-Code zeigt PROV-Nummer + +**Symptom:** QR-Code enthält "PROV23" statt "IN26-0001" + +**Ursache:** Object wurde nicht neu geladen im Trigger + +**Lösung:** Prüfe in `actions_epcqr.class.php`: + +```php +// FALSCH: +$ref = $object->ref; + +// RICHTIG: +$invoice = new Facture($this->db); +$invoice->fetch($object->id); +$ref = $invoice->ref; +``` + +### Problem: Extra-Felder sind leer + +**Symptom:** Nach Freigabe sind die Felder `qrcode` und `qrcodepfad` leer + +**Lösung:** +1. Modul **deaktivieren** +2. Modul wieder **aktivieren** +3. Prüfe: **Setup → Dictionaries → Extra Attributes → Invoices** +4. Sollten 2 Felder da sein: `qrcode` und `qrcodepfad` + +### Problem: QR-Code wird in ODT nicht angezeigt + +**Symptom:** Template zeigt nur `{invoice_options_qrcode}` als Text + +**Lösungen:** +1. **Feldname prüfen:** Muss exakt `{invoice_options_qrcode}` heißen +2. **Extra-Feld aktiviert?** Setup → Extra Attributes → "Printable" = Ja +3. **Template neu generieren:** Rechnung → "Generate document" + +### Problem: Falscher Betrag im QR-Code + +**Symptom:** Banking-App zeigt falschen Betrag + +**Ursache:** Formatierung falsch + +**Lösung:** Modul nutzt korrekt `price2num($invoice->total_ttc, 'MT')` + +### Problem: QR-Code kann nicht gescannt werden + +**Mögliche Ursachen:** +- QR-Code zu klein im PDF → Größe in ODT anpassen +- Externe URL nicht erreichbar → Internetverbindung prüfen +- Falsches Format → Service-URL prüfen + +--- + +## Extra-Felder Details + +### Feld: qrcode + +- **Typ:** HTML +- **Inhalt:** `` +- **Verwendung:** Für ODT-Templates +- **Printable:** Ja +- **Position:** 100 + +### Feld: qrcodepfad + +- **Typ:** varchar(255) +- **Inhalt:** Vollständige QR-Code URL +- **Verwendung:** Backup, Debugging, externe Verarbeitung +- **Printable:** Nein (nur intern) +- **Position:** 101 + +--- + +## Code-Referenz + +### Kompletter Trigger-Code + +```php +public function runTrigger($action, $object, User $user, Translate $langs, Conf $conf) +{ + if ($action == 'BILL_VALIDATE') { + + dol_syslog("EPCQR: Generiere QR-Code für Rechnung ID " . $object->id); + + // ⚠️ WICHTIG: Objekt neu laden für finale Rechnungsnummer! + require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php'; + $invoice = new Facture($this->db); + $result = $invoice->fetch($object->id); + + if ($result > 0) { + // Bankdaten - HIER ANPASSEN! + $accountHolder = 'Eduard Wisch'; + $iban = 'DE70217625500013438147'; + $bic = 'GENODEF1HUM'; + $amount = price2num($invoice->total_ttc, 'MT'); + $ref = $invoice->ref; // Finale Nummer + + // QR-Code URL generieren + $qrurl = "https://qr.data-it-solution.de/epc?" + . "name=" . urlencode($accountHolder) + . "&iban=" . urlencode($iban) + . "&bic=" . urlencode($bic) + . "&amount=" . urlencode($amount) + . "&remittance=" . urlencode($ref); + + // In Extra-Felder speichern + $invoice->array_options['options_qrcode'] = ""; + $invoice->insertExtraFields(); + $invoice->array_options['options_qrcodepfad'] = $qrurl; + $invoice->insertExtraFields(); + + dol_syslog("EPCQR: QR-Code generiert für " . $invoice->ref); + } else { + dol_syslog("EPCQR: Fehler beim Laden der Rechnung", LOG_ERR); + return -1; + } + + return 1; + } + + return 0; +} +``` + +--- + +## FAQ + +### Kann ich andere QR-Services nutzen? + +Ja! Ändere einfach die URL in `actions_epcqr.class.php`: + +```php +$qrurl = "https://dein-service.de/qr?" + . "deine_parameter=" . urlencode($value); +``` + +### Funktioniert das Modul mit allen Rechnungstypen? + +Ja, der Trigger `BILL_VALIDATE` wird für alle Rechnungstypen aufgerufen: +- Standard-Rechnungen +- Gutschriften +- Ersatzrechnungen +- Anzahlungsrechnungen + +### Kann ich die QR-Code-Größe ändern? + +Ja, in 2 Varianten: + +**1. Im Code:** +```php +$invoice->array_options['options_qrcode'] = ""; +``` + +**2. Im ODT-Template:** +Bild markieren → Rechtsklick → Eigenschaften → Größe ändern + +### Werden alte Rechnungen auch mit QR-Codes versehen? + +Nein, nur neue Rechnungen die nach Modulaktivierung **freigegeben** werden. + +**Für alte Rechnungen:** +1. Rechnung auf "Draft" setzen +2. Erneut freigeben +3. QR-Code wird generiert + +--- + +## Support & Kontakt + +**Entwickler:** Eduard Wisch +**GitHub:** [Dein GitHub Link] +**E-Mail:** [Deine E-Mail] + +### Bei Problemen + +1. **Log-Dateien prüfen:** `documents/dolibarr.log` +2. **Debug aktivieren:** Setup → Other → Enable debug mode +3. **GitHub Issues:** [Link zu Issues] + +--- + +## Changelog + +### Version 1.0 (2026-01-11) +- ✅ Erste stabile Version +- ✅ Automatische QR-Code Generierung +- ✅ Extra-Felder für ODT-Integration +- ✅ Externe QR-Service Integration +- ✅ Object-Reload Fix für finale Rechnungsnummer + +--- + +## Lizenz + +GPL-3.0+ + +Dieses Modul ist freie Software; Sie können es unter den Bedingungen der GNU General Public License Version 3 oder später weitergeben und/oder modifizieren. + +--- + +**Viel Erfolg mit EPCQR!** 🎉 diff --git a/admin/about.php b/admin/about.php new file mode 100755 index 0000000..b868fc4 --- /dev/null +++ b/admin/about.php @@ -0,0 +1,118 @@ + + * Copyright (C) 2025 Eduard Wisch + * Copyright (C) 2024 Frédéric France + * + * 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 . + */ + +/** + * \file epcqr/admin/about.php + * \ingroup epcqr + * \brief About page of module Epcqr. + */ + +// Load Dolibarr environment +$res = 0; +// Try main.inc.php into web root known defined into CONTEXT_DOCUMENT_ROOT (not always defined) +if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) { + $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php"; +} +// Try main.inc.php into web root detected using web root calculated from SCRIPT_FILENAME +$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; +$tmp2 = realpath(__FILE__); +$i = strlen($tmp) - 1; +$j = strlen($tmp2) - 1; +while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { + $i--; + $j--; +} +if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) { + $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php"; +} +if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) { + $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php"; +} +// Try main.inc.php using relative path +if (!$res && file_exists("../../main.inc.php")) { + $res = @include "../../main.inc.php"; +} +if (!$res && file_exists("../../../main.inc.php")) { + $res = @include "../../../main.inc.php"; +} +if (!$res) { + die("Include of main fails"); +} + +// Libraries +require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php'; +require_once DOL_DOCUMENT_ROOT.'/core/lib/functions2.lib.php'; +require_once '../lib/epcqr.lib.php'; + +/** + * @var Conf $conf + * @var DoliDB $db + * @var HookManager $hookmanager + * @var Translate $langs + * @var User $user + */ + +// Translations +$langs->loadLangs(array("errors", "admin", "epcqr@epcqr")); + +// Access control +if (!$user->admin) { + accessforbidden(); +} + +// Parameters +$action = GETPOST('action', 'aZ09'); +$backtopage = GETPOST('backtopage', 'alpha'); + + +/* + * Actions + */ + +// None + + +/* + * View + */ + +$form = new Form($db); + +$help_url = ''; +$title = "EpcqrSetup"; + +llxHeader('', $langs->trans($title), $help_url, '', 0, 0, '', '', '', 'mod-epcqr page-admin_about'); + +// Subheader +$linkback = ''.$langs->trans("BackToModuleList").''; + +print load_fiche_titre($langs->trans($title), $linkback, 'title_setup'); + +// Configuration header +$head = epcqrAdminPrepareHead(); +print dol_get_fiche_head($head, 'about', $langs->trans($title), 0, 'epcqr@epcqr'); + +dol_include_once('/epcqr/core/modules/modEpcqr.class.php'); +$tmpmodule = new modEpcqr($db); +print $tmpmodule->getDescLong(); + +// Page end +print dol_get_fiche_end(); +llxFooter(); +$db->close(); diff --git a/admin/setup.php b/admin/setup.php new file mode 100755 index 0000000..17188b7 --- /dev/null +++ b/admin/setup.php @@ -0,0 +1,629 @@ + + * Copyright (C) 2024 Frédéric France + * Copyright (C) 2025 Eduard Wisch + * + * 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 . + */ + +/** + * \file epcqr/admin/setup.php + * \ingroup epcqr + * \brief Epcqr setup page. + */ + +// Load Dolibarr environment +$res = 0; +// Try main.inc.php into web root known defined into CONTEXT_DOCUMENT_ROOT (not always defined) +if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) { + $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php"; +} +// Try main.inc.php into web root detected using web root calculated from SCRIPT_FILENAME +$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; +$tmp2 = realpath(__FILE__); +$i = strlen($tmp) - 1; +$j = strlen($tmp2) - 1; +while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { + $i--; + $j--; +} +if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) { + $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php"; +} +if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) { + $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php"; +} +// Try main.inc.php using relative path +if (!$res && file_exists("../../main.inc.php")) { + $res = @include "../../main.inc.php"; +} +if (!$res && file_exists("../../../main.inc.php")) { + $res = @include "../../../main.inc.php"; +} +if (!$res) { + die("Include of main fails"); +} + +// Libraries +require_once DOL_DOCUMENT_ROOT."/core/lib/admin.lib.php"; +require_once '../lib/epcqr.lib.php'; +//require_once "../class/myclass.class.php"; + +/** + * @var Conf $conf + * @var DoliDB $db + * @var HookManager $hookmanager + * @var Translate $langs + * @var User $user + */ + +// Translations +$langs->loadLangs(array("admin", "epcqr@epcqr")); + +// Initialize a technical object to manage hooks of page. Note that conf->hooks_modules contains an array of hook context +/** @var HookManager $hookmanager */ +$hookmanager->initHooks(array('epcqrsetup', 'globalsetup')); + +// Parameters +$action = GETPOST('action', 'aZ09'); +$backtopage = GETPOST('backtopage', 'alpha'); +$modulepart = GETPOST('modulepart', 'aZ09'); // Used by actions_setmoduleoptions.inc.php + +$value = GETPOST('value', 'alpha'); +$label = GETPOST('label', 'alpha'); +$scandir = GETPOST('scan_dir', 'alpha'); +$type = 'myobject'; + +$error = 0; +$setupnotempty = 0; + +// Access control +if (!$user->admin) { + accessforbidden(); +} + + +// Set this to 1 to use the factory to manage constants. Warning, the generated module will be compatible with version v15+ only +$useFormSetup = 1; + +if (!class_exists('FormSetup')) { + require_once DOL_DOCUMENT_ROOT.'/core/class/html.formsetup.class.php'; +} +$formSetup = new FormSetup($db); + +// Access control +if (!$user->admin) { + accessforbidden(); +} + + +// Enter here all parameters in your setup page + +// Setup conf for selection of an URL +$item = $formSetup->newItem('EPCQR_MYPARAM1'); +$item->fieldParams['isMandatory'] = 1; +$item->fieldAttr['placeholder'] = (empty($_SERVER['HTTPS']) ? 'http://' : 'https://') . $_SERVER['HTTP_HOST']; +$item->cssClass = 'minwidth500'; + +// Setup conf for selection of a simple string input +$item = $formSetup->newItem('EPCQR_MYPARAM2'); +$item->defaultFieldValue = 'default value'; +$item->fieldAttr['placeholder'] = 'A placeholder here'; +$item->helpText = 'Tooltip text'; + +// Setup conf for selection of a simple textarea input but we replace the text of field title +$item = $formSetup->newItem('EPCQR_MYPARAM3'); +$item->nameText = $item->getNameText().' more html text '; + +// Setup conf for a selection of a Thirdparty +$item = $formSetup->newItem('EPCQR_MYPARAM4'); +$item->setAsThirdpartyType(); + +// Setup conf for a selection of a boolean +$formSetup->newItem('EPCQR_MYPARAM5')->setAsYesNo(); + +// Setup conf for a selection of an Email template of type thirdparty +$formSetup->newItem('EPCQR_MYPARAM6')->setAsEmailTemplate('thirdparty'); + +// Setup conf for a selection of a secured key +//$formSetup->newItem('EPCQR_MYPARAM7')->setAsSecureKey(); + +// Setup conf for a selection of a Product +$formSetup->newItem('EPCQR_MYPARAM8')->setAsProduct(); + +// Add a title for a new section +$formSetup->newItem('NewSection')->setAsTitle(); + +$TField = array( + 'test01' => $langs->trans('test01'), + 'test02' => $langs->trans('test02'), + 'test03' => $langs->trans('test03'), + 'test04' => $langs->trans('test04'), + 'test05' => $langs->trans('test05'), + 'test06' => $langs->trans('test06'), +); + +// Setup conf for a simple combo list +$formSetup->newItem('EPCQR_MYPARAM9')->setAsSelect($TField); + +// Setup conf for a multiselect combo list +$item = $formSetup->newItem('EPCQR_MYPARAM10'); +$item->setAsMultiSelect($TField); +$item->helpText = $langs->transnoentities('EPCQR_MYPARAM10'); + +// Setup conf for a category selection +$formSetup->newItem('EPCQR_CATEGORY_ID_XXX')->setAsCategory('product'); + +// Setup conf EPCQR_MYPARAM10 +$item = $formSetup->newItem('EPCQR_MYPARAM10'); +$item->setAsColor(); +$item->defaultFieldValue = '#FF0000'; +//$item->fieldValue = ''; +//$item->fieldAttr = array() ; // fields attribute only for compatible fields like input text +//$item->fieldOverride = false; // set this var to override field output will override $fieldInputOverride and $fieldOutputOverride too +//$item->fieldInputOverride = false; // set this var to override field input +//$item->fieldOutputOverride = false; // set this var to override field output + +$item = $formSetup->newItem('EPCQR_MYPARAM11')->setAsHtml(); +$item->nameText = $item->getNameText().' more html text '; +$item->fieldInputOverride = ''; +$item->helpText = $langs->transnoentities('HelpMessage'); +$item->cssClass = 'minwidth500'; + +$item = $formSetup->newItem('EPCQR_MYPARAM12'); +$item->fieldOverride = "Value forced, can't be modified"; +$item->cssClass = 'minwidth500'; + +//$item = $formSetup->newItem('EPCQR_MYPARAM13')->setAsDate(); // Not yet implemented + +// End of definition of parameters + + +$setupnotempty += count($formSetup->items); + + +$dirmodels = array_merge(array('/'), (array) $conf->modules_parts['models']); + +$moduledir = 'epcqr'; +$myTmpObjects = array(); +// TODO Scan list of objects to fill this array +$myTmpObjects['myobject'] = array('label' => 'MyObject', 'includerefgeneration' => 0, 'includedocgeneration' => 0, 'class' => 'MyObject'); + +$tmpobjectkey = GETPOST('object', 'aZ09'); +if ($tmpobjectkey && !array_key_exists($tmpobjectkey, $myTmpObjects)) { + accessforbidden('Bad value for object. Hack attempt ?'); +} + + +/* + * Actions + */ + +// For retrocompatibility Dolibarr < 15.0 +if (versioncompare(explode('.', DOL_VERSION), array(15)) < 0 && $action == 'update' && !empty($user->admin)) { + $formSetup->saveConfFromPost(); +} + +include DOL_DOCUMENT_ROOT.'/core/actions_setmoduleoptions.inc.php'; + +if ($action == 'updateMask') { + $maskconst = GETPOST('maskconst', 'aZ09'); + $maskvalue = GETPOST('maskvalue', 'alpha'); + + if ($maskconst && preg_match('/_MASK$/', $maskconst)) { + $res = dolibarr_set_const($db, $maskconst, $maskvalue, 'chaine', 0, '', $conf->entity); + if (!($res > 0)) { + $error++; + } + } + + if (!$error) { + setEventMessages($langs->trans("SetupSaved"), null, 'mesgs'); + } else { + setEventMessages($langs->trans("Error"), null, 'errors'); + } +} elseif ($action == 'specimen' && $tmpobjectkey) { + $modele = GETPOST('module', 'alpha'); + + $className = $myTmpObjects[$tmpobjectkey]['class']; + $tmpobject = new $className($db); + '@phan-var-force MyObject $tmpobject'; + $tmpobject->initAsSpecimen(); + + // Search template files + $file = ''; + $className = ''; + $dirmodels = array_merge(array('/'), (array) $conf->modules_parts['models']); + foreach ($dirmodels as $reldir) { + $file = dol_buildpath($reldir."core/modules/epcqr/doc/pdf_".$modele."_".strtolower($tmpobjectkey).".modules.php", 0); + if (file_exists($file)) { + $className = "pdf_".$modele."_".strtolower($tmpobjectkey); + break; + } + } + + if ($className !== '') { + require_once $file; + + $module = new $className($db); + '@phan-var-force ModelePDFMyObject $module'; + + '@phan-var-force ModelePDFMyObject $module'; + + if ($module->write_file($tmpobject, $langs) > 0) { + header("Location: ".DOL_URL_ROOT."/document.php?modulepart=epcqr-".strtolower($tmpobjectkey)."&file=SPECIMEN.pdf"); + return; + } else { + setEventMessages($module->error, null, 'errors'); + dol_syslog($module->error, LOG_ERR); + } + } else { + setEventMessages($langs->trans("ErrorModuleNotFound"), null, 'errors'); + dol_syslog($langs->trans("ErrorModuleNotFound"), LOG_ERR); + } +} elseif ($action == 'setmod') { + // TODO Check if numbering module chosen can be activated by calling method canBeActivated + if (!empty($tmpobjectkey)) { + $constforval = 'EPCQR_'.strtoupper($tmpobjectkey)."_ADDON"; + dolibarr_set_const($db, $constforval, $value, 'chaine', 0, '', $conf->entity); + } +} elseif ($action == 'set') { + // Activate a model + $ret = addDocumentModel($value, $type, $label, $scandir); +} elseif ($action == 'del') { + $ret = delDocumentModel($value, $type); + if ($ret > 0) { + if (!empty($tmpobjectkey)) { + $constforval = 'EPCQR_'.strtoupper($tmpobjectkey).'_ADDON_PDF'; + if (getDolGlobalString($constforval) == "$value") { + dolibarr_del_const($db, $constforval, $conf->entity); + } + } + } +} elseif ($action == 'setdoc') { + // Set or unset default model + if (!empty($tmpobjectkey)) { + $constforval = 'EPCQR_'.strtoupper($tmpobjectkey).'_ADDON_PDF'; + if (dolibarr_set_const($db, $constforval, $value, 'chaine', 0, '', $conf->entity)) { + // The constant that was read before the new set + // We therefore requires a variable to have a coherent view + $conf->global->{$constforval} = $value; + } + + // We disable/enable the document template (into llx_document_model table) + $ret = delDocumentModel($value, $type); + if ($ret > 0) { + $ret = addDocumentModel($value, $type, $label, $scandir); + } + } +} elseif ($action == 'unsetdoc') { + if (!empty($tmpobjectkey)) { + $constforval = 'EPCQR_'.strtoupper($tmpobjectkey).'_ADDON_PDF'; + dolibarr_del_const($db, $constforval, $conf->entity); + } +} + +$action = 'edit'; + + +/* + * View + */ + +$form = new Form($db); + +$help_url = ''; +$title = "EpcqrSetup"; + +llxHeader('', $langs->trans($title), $help_url, '', 0, 0, '', '', '', 'mod-epcqr page-admin'); + +// Subheader +$linkback = ''.$langs->trans("BackToModuleList").''; + +print load_fiche_titre($langs->trans($title), $linkback, 'title_setup'); + +// Configuration header +$head = epcqrAdminPrepareHead(); +print dol_get_fiche_head($head, 'settings', $langs->trans($title), -1, "epcqr@epcqr"); + +// Setup page goes here +echo ''.$langs->trans("EpcqrSetupPage").'

'; + + +/*if ($action == 'edit') { + print $formSetup->generateOutput(true); + print '
'; + } elseif (!empty($formSetup->items)) { + print $formSetup->generateOutput(); + print '
'; + print ''.$langs->trans("Modify").''; + print '
'; + } + */ +if (!empty($formSetup->items)) { + print $formSetup->generateOutput(true); + print '
'; +} + + +foreach ($myTmpObjects as $myTmpObjectKey => $myTmpObjectArray) { + if (!empty($myTmpObjectArray['includerefgeneration'])) { + // Numbering models + + $setupnotempty++; + + print load_fiche_titre($langs->trans("NumberingModules", $myTmpObjectArray['label']), '', ''); + + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''."\n"; + + clearstatcache(); + + foreach ($dirmodels as $reldir) { + $dir = dol_buildpath($reldir."core/modules/".$moduledir); + + if (is_dir($dir)) { + $handle = opendir($dir); + if (is_resource($handle)) { + while (($file = readdir($handle)) !== false) { + if (strpos($file, 'mod_'.strtolower($myTmpObjectKey).'_') === 0 && substr($file, dol_strlen($file) - 3, 3) == 'php') { + $file = substr($file, 0, dol_strlen($file) - 4); + + require_once $dir.'/'.$file.'.php'; + + $module = new $file($db); + '@phan-var-force ModeleNumRefMyObject $module'; + + // Show modules according to features level + if ($module->version == 'development' && getDolGlobalInt('MAIN_FEATURES_LEVEL') < 2) { + continue; + } + if ($module->version == 'experimental' && getDolGlobalInt('MAIN_FEATURES_LEVEL') < 1) { + continue; + } + + if ($module->isEnabled()) { + dol_include_once('/'.$moduledir.'/class/'.strtolower($myTmpObjectKey).'.class.php'); + + print ''; + + // Show example of numbering model + print ''."\n"; + + print ''; + + $className = $myTmpObjectArray['class']; + $mytmpinstance = new $className($db); + '@phan-var-force MyObject $mytmpinstance'; + $mytmpinstance->initAsSpecimen(); + + // Info + $htmltooltip = ''; + $htmltooltip .= ''.$langs->trans("Version").': '.$module->getVersion().'
'; + + $nextval = $module->getNextValue($mytmpinstance); + if ("$nextval" != $langs->trans("NotAvailable")) { // Keep " on nextval + $htmltooltip .= ''.$langs->trans("NextValue").': '; + if ($nextval) { + if (preg_match('/^Error/', $nextval) || $nextval == 'NotConfigured') { + $nextval = $langs->trans($nextval); + } + $htmltooltip .= $nextval.'
'; + } else { + $htmltooltip .= $langs->trans($module->error).'
'; + } + } + + print ''; + + print "\n"; + } + } + } + closedir($handle); + } + } + } + print "
'.$langs->trans("Name").''.$langs->trans("Description").''.$langs->trans("Example").''.$langs->trans("Status").''.$langs->trans("ShortInfo").'
'.$module->getName($langs)."\n"; + print $module->info($langs); + print ''; + $tmp = $module->getExample(); + if (preg_match('/^Error/', $tmp)) { + $langs->load("errors"); + print '
'.$langs->trans($tmp).'
'; + } elseif ($tmp == 'NotConfigured') { + print $langs->trans($tmp); + } else { + print $tmp; + } + print '
'; + $constforvar = 'EPCQR_'.strtoupper($myTmpObjectKey).'_ADDON'; + $defaultifnotset = 'thevaluetousebydefault'; + $activenumberingmodel = getDolGlobalString($constforvar, $defaultifnotset); + if ($activenumberingmodel == $file) { + print img_picto($langs->trans("Activated"), 'switch_on'); + } else { + print ''; + print img_picto($langs->trans("Disabled"), 'switch_off'); + print ''; + } + print ''; + print $form->textwithpicto('', $htmltooltip, 1, 'info'); + print '

\n"; + } + + if (!empty($myTmpObjectArray['includedocgeneration'])) { + /* + * Document templates generators + */ + $setupnotempty++; + $type = strtolower($myTmpObjectKey); + + print load_fiche_titre($langs->trans("DocumentModules", $myTmpObjectKey), '', ''); + + // Load array def with activated templates + $def = array(); + $sql = "SELECT nom"; + $sql .= " FROM ".$db->prefix()."document_model"; + $sql .= " WHERE type = '".$db->escape($type)."'"; + $sql .= " AND entity = ".$conf->entity; + $resql = $db->query($sql); + if ($resql) { + $i = 0; + $num_rows = $db->num_rows($resql); + while ($i < $num_rows) { + $array = $db->fetch_array($resql); + array_push($def, $array[0]); + $i++; + } + } else { + dol_print_error($db); + } + + print ''."\n"; + print ''."\n"; + print ''; + print ''; + print '\n"; + print '\n"; + print ''; + print ''; + print "\n"; + + clearstatcache(); + + foreach ($dirmodels as $reldir) { + foreach (array('', '/doc') as $valdir) { + $realpath = $reldir."core/modules/".$moduledir.$valdir; + $dir = dol_buildpath($realpath); + + if (is_dir($dir)) { + $handle = opendir($dir); + if (is_resource($handle)) { + $filelist = array(); + while (($file = readdir($handle)) !== false) { + $filelist[] = $file; + } + closedir($handle); + arsort($filelist); + + foreach ($filelist as $file) { + if (preg_match('/\.modules\.php$/i', $file) && preg_match('/^(pdf_|doc_)/', $file)) { + if (file_exists($dir.'/'.$file)) { + $name = substr($file, 4, dol_strlen($file) - 16); + $className = substr($file, 0, dol_strlen($file) - 12); + + require_once $dir.'/'.$file; + $module = new $className($db); + '@phan-var-force ModelePDFMyObject $module'; + + $modulequalified = 1; + if ($module->version == 'development' && getDolGlobalInt('MAIN_FEATURES_LEVEL') < 2) { + $modulequalified = 0; + } + if ($module->version == 'experimental' && getDolGlobalInt('MAIN_FEATURES_LEVEL') < 1) { + $modulequalified = 0; + } + + if ($modulequalified) { + print ''; + + // Active + if (in_array($name, $def)) { + print ''; + } else { + print '"; + } + + // Default + print ''; + + // Info + $htmltooltip = ''.$langs->trans("Name").': '.$module->name; + $htmltooltip .= '
'.$langs->trans("Type").': '.($module->type ? $module->type : $langs->trans("Unknown")); + if ($module->type == 'pdf') { + $htmltooltip .= '
'.$langs->trans("Width").'/'.$langs->trans("Height").': '.$module->page_largeur.'/'.$module->page_hauteur; + } + $htmltooltip .= '
'.$langs->trans("Path").': '.preg_replace('/^\//', '', $realpath).'/'.$file; + + $htmltooltip .= '

'.$langs->trans("FeaturesSupported").':'; + $htmltooltip .= '
'.$langs->trans("Logo").': '.yn($module->option_logo, 1, 1); + $htmltooltip .= '
'.$langs->trans("MultiLanguage").': '.yn($module->option_multilang, 1, 1); + + print ''; + + // Preview + print ''; + + print "\n"; + } + } + } + } + } + } + } + } + + print '
'.$langs->trans("Name").''.$langs->trans("Description").''.$langs->trans("Status")."'.$langs->trans("Default")."'.$langs->trans("ShortInfo").''.$langs->trans("Preview").'
'; + print(empty($module->name) ? $name : $module->name); + print "\n"; + if (method_exists($module, 'info')) { + print $module->info($langs); // @phan-suppress-current-line PhanUndeclaredMethod + } else { + print $module->description; + } + print ''."\n"; + print ''; + print img_picto($langs->trans("Enabled"), 'switch_on'); + print ''; + print ''."\n"; + print 'scandir).'&label='.urlencode($module->name).'">'.img_picto($langs->trans("Disabled"), 'switch_off').''; + print "'; + $constforvar = 'EPCQR_'.strtoupper($myTmpObjectKey).'_ADDON_PDF'; + if (getDolGlobalString($constforvar) == $name) { + //print img_picto($langs->trans("Default"), 'on'); + // Even if choice is the default value, we allow to disable it. Replace this with previous line if you need to disable unset + print 'scandir).'&label='.urlencode($module->name).'&type='.urlencode($type).'" alt="'.$langs->trans("Disable").'">'.img_picto($langs->trans("Enabled"), 'on').''; + } else { + print 'scandir).'&label='.urlencode($module->name).'" alt="'.$langs->trans("Default").'">'.img_picto($langs->trans("Disabled"), 'off').''; + } + print ''; + print $form->textwithpicto('', $htmltooltip, 1, 'info'); + print ''; + if ($module->type == 'pdf') { + $newname = preg_replace('/_'.preg_quote(strtolower($myTmpObjectKey), '/').'/', '', $name); + print ''.img_object($langs->trans("Preview"), 'pdf').''; + } else { + print img_object($langs->transnoentitiesnoconv("PreviewNotAvailable"), 'generic'); + } + print '
'; + } +} + +if (empty($setupnotempty)) { + print '
'.$langs->trans("NothingToSetup"); +} + +// Page end +print dol_get_fiche_end(); + +llxFooter(); +$db->close(); diff --git a/bin/module_epcqr-1.4.zip b/bin/module_epcqr-1.4.zip new file mode 100755 index 0000000..ac6907f Binary files /dev/null and b/bin/module_epcqr-1.4.zip differ diff --git a/build/buildzip.php b/build/buildzip.php new file mode 100755 index 0000000..3508bbb --- /dev/null +++ b/build/buildzip.php @@ -0,0 +1,316 @@ +#!/usr/bin/env php -d memory_limit=256M + + * + * 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 . + */ + +/* + The goal of that php CLI script is to make zip package of your module + as an alternative to web "build zip" or "perl script makepack" +*/ + +// ============================================= configuration + +/** + * list of files & dirs of your module + * + * @var string[] + */ +$listOfModuleContent = [ + 'admin', + 'ajax', + 'backport', + 'class', + 'css', + 'COPYING', + 'core', + 'img', + 'js', + 'langs', + 'lib', + 'sql', + 'tpl', + '*.md', + '*.json', + '*.php', + 'modulebuilder.txt', +]; + +/** + * if you want to exclude some files from your zip + * + * @var string[] + */ +$exclude_list = [ + '/^.git$/', + '/.*js.map/', + '/DEV.md/' +]; + +// ============================================= end of configuration + +/** + * auto detect module name and version from file name + * + * @return (string|string)[] module name and module version + */ +function detectModule() +{ + $name = $version = ""; + $tab = glob("core/modules/mod*.class.php"); + if (count($tab) == 0) { + echo "[fail] Error on auto detect data : there is no mod*.class.php file into core/modules dir\n"; + exit(-1); + } + if (count($tab) == 1) { + $file = $tab[0]; + $pattern = "/.*mod(?.*)\.class\.php/"; + if (preg_match_all($pattern, $file, $matches)) { + $name = strtolower(reset($matches['mod'])); + } + + echo "extract data from $file\n"; + if (!file_exists($file) || $name == "") { + echo "[fail] Error on auto detect data\n"; + exit(-2); + } + } else { + echo "[fail] Error there is more than one mod*.class.php file into core/modules dir\n"; + exit(-3); + } + + //extract version from file + $contents = file_get_contents($file); + $pattern = "/^.*this->version\s*=\s*'(?.*)'\s*;.*\$/m"; + + // search, and store all matching occurrences in $matches + if (preg_match_all($pattern, $contents, $matches)) { + $version = reset($matches['version']); + } + + if (version_compare($version, '0.0.1', '>=') != 1) { + echo "[fail] Error auto extract version fail\n"; + exit(-4); + } + + echo "module name = $name, version = $version\n"; + return [(string) $name, (string) $version]; +} + +/** + * delete recursively a directory + * + * @param string $dir dir path to delete + * + * @return bool true on success or false on failure. + */ +function delTree($dir) +{ + $files = array_diff(scandir($dir), array('.', '..')); + foreach ($files as $file) { + (is_dir("$dir/$file")) ? delTree("$dir/$file") : secureUnlink("$dir/$file"); + } + return rmdir($dir); +} + + +/** + * do a secure delete file/dir with double check + * (don't trust unlink return) + * + * @param string $path full path to delete + * + * @return bool true on success ($path does not exists at the end of process), else exit + */ +function secureUnlink($path) +{ + if (file_exists($path)) { + if (unlink($path)) { + //then check if really deleted + clearstatcache(); + if (file_exists($path)) { // @phpstan-ignore-line + echo "[fail] unlink of $path fail !\n"; + exit(-5); + } + } else { + echo "[fail] unlink of $path fail !\n"; + exit(-6); + } + } + return true; +} + +/** + * create a directory and check if dir exists + * + * @param string $path path to make + * + * @return bool true on success ($path exists at the end of process), else exit + */ +function mkdirAndCheck($path) +{ + if (mkdir($path)) { + clearstatcache(); + if (is_dir($path)) { + return true; + } + } + echo "[fail] Error on $path (mkdir)\n"; + exit(7); +} + +/** + * check if that filename is concerned by exclude filter + * + * @param string $filename file name to check + * + * @return bool true if file is in excluded list + */ +function is_excluded($filename) +{ + global $exclude_list; + $count = 0; + $notused = preg_filter($exclude_list, '1', $filename, -1, $count); + if ($count > 0) { + echo " - exclude $filename\n"; + return true; + } + return false; +} + +/** + * recursive copy files & dirs + * + * @param string $src source dir + * @param string $dst target dir + * + * @return bool true on success or false on failure. + */ +function rcopy($src, $dst) +{ + if (is_dir($src)) { + // Make the destination directory if not exist + mkdirAndCheck($dst); + // open the source directory + $dir = opendir($src); + + // Loop through the files in source directory + while ($file = readdir($dir)) { + if (($file != '.') && ($file != '..')) { + if (is_dir($src . '/' . $file)) { + // Recursively calling custom copy function + // for sub directory + if (!rcopy($src . '/' . $file, $dst . '/' . $file)) { + return false; + } + } else { + if (!is_excluded($file)) { + if (!copy($src . '/' . $file, $dst . '/' . $file)) { + return false; + } + } + } + } + } + closedir($dir); + } elseif (is_file($src)) { + if (!is_excluded($src)) { + if (!copy($src, $dst)) { + return false; + } + } + } + return true; +} + +/** + * build a zip file with only php code and no external depends + * on "zip" exec for example + * + * @param string $folder folder to use as zip root + * @param ZipArchive $zip zip object (ZipArchive) + * @param string $root relative root path into the zip + * + * @return bool true on success or false on failure. + */ +function zipDir($folder, &$zip, $root = "") +{ + foreach (new \DirectoryIterator($folder) as $f) { + if ($f->isDot()) { + continue; + } //skip . .. + $src = $folder . '/' . $f; + $dst = substr($f->getPathname(), strlen($root)); + if ($f->isDir()) { + if ($zip->addEmptyDir($dst)) { + if (zipDir($src, $zip, $root)) { + continue; + } else { + return false; + } + } else { + return false; + } + } + if ($f->isFile()) { + if (! $zip->addFile($src, $dst)) { + return false; + } + } + } + return true; +} + +/** + * main part of script + */ + +list($mod, $version) = detectModule(); +$outzip = sys_get_temp_dir() . "/module_" . $mod . "-" . $version . ".zip"; +if (file_exists($outzip)) { + secureUnlink($outzip); +} + +//copy all sources into system temp directory +$tmpdir = tempnam(sys_get_temp_dir(), $mod . "-module"); +secureUnlink($tmpdir); +mkdirAndCheck($tmpdir); +$dst = $tmpdir . "/" . $mod; +mkdirAndCheck($dst); + +foreach ($listOfModuleContent as $moduleContent) { + foreach (glob($moduleContent) as $entry) { + if (!rcopy($entry, $dst . '/' . $entry)) { + echo "[fail] Error on copy " . $entry . " to " . $dst . "/" . $entry . "\n"; + echo "Please take time to analyze the problem and fix the bug\n"; + exit(-8); + } + } +} + +$z = new ZipArchive(); +$z->open($outzip, ZIPARCHIVE::CREATE); +zipDir($tmpdir, $z, $tmpdir . '/'); +$z->close(); +delTree($tmpdir); +if (file_exists($outzip)) { + echo "[success] module archive is ready : $outzip ...\n"; +} else { + echo "[fail] build zip error\n"; + exit(-9); +} diff --git a/build/makepack-epcqr.conf b/build/makepack-epcqr.conf new file mode 100755 index 0000000..16dc1e7 --- /dev/null +++ b/build/makepack-epcqr.conf @@ -0,0 +1,11 @@ +# Your module name here +# +# Goal: Goal of module +# Version: +# Author: Copyright - +# License: GPLv3 +# Install: Just unpack content of module package in Dolibarr directory. +# Setup: Go on Dolibarr setup - modules to enable module. +# +# Files in module +mymodule/ \ No newline at end of file diff --git a/class/actions_epcqr.class.php b/class/actions_epcqr.class.php new file mode 100644 index 0000000..f96f627 --- /dev/null +++ b/class/actions_epcqr.class.php @@ -0,0 +1,147 @@ + + * + * 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 . + */ + +/** + * \file class/actions_epcqr.class.php + * \ingroup epcqr + * \brief Hook-Klasse für QR-Code und Bild-Integration + */ + +/** + * Hook-Klasse für EPCQR-Modul + * Verarbeitet ODT-Dokumente und fügt Bilder/QR-Codes ein + */ +class ActionsEpcqr +{ + private $db; + private $conf; + private $langs; + public $errors = array(); + public $resprints; + + /** + * Constructor + * + * @param DoliDB $db Database handler + */ + public function __construct($db) + { + $this->db = $db; + + global $conf, $langs; + $this->conf = $conf; + $this->langs = $langs; + } + + /** + * Hook nach ODT-Generierung + * Wird aufgerufen nachdem ein ODT-Dokument erstellt wurde + * + * @param array $parameters Hook-Parameter + * @param object $object Dolibarr-Objekt + * @param string $action Aktuelle Aktion + * @return int 0 = OK, <0 = Fehler + */ + public function afterODTCreation($parameters, &$object, &$action) + { + global $conf; + + dol_syslog("EPCQR Hook: afterODTCreation aufgerufen", LOG_DEBUG); + + // Prüfen ob Parameter vorhanden sind + if (empty($parameters['file'])) { + dol_syslog("EPCQR Hook: Kein Dateipfad in Parametern", LOG_DEBUG); + return 0; + } + + $odfFilePath = $parameters['file']; + + // Prüfen ob Datei existiert + if (!file_exists($odfFilePath)) { + dol_syslog("EPCQR Hook: ODT-Datei existiert nicht: ".$odfFilePath, LOG_WARNING); + return 0; + } + + // Nur ODT-Dateien verarbeiten + if (pathinfo($odfFilePath, PATHINFO_EXTENSION) !== 'odt') { + dol_syslog("EPCQR Hook: Keine ODT-Datei, überspringe", LOG_DEBUG); + return 0; + } + + // Prüfen ob Objekt gültig ist + if (!is_object($object) || empty($object->id)) { + dol_syslog("EPCQR Hook: Ungültiges Objekt", LOG_DEBUG); + return 0; + } + + // Bilder in ODT einfügen + require_once __DIR__.'/../lib/epcqr.lib.php'; + $result = epcqr_processODTImages($odfFilePath, $object); + + if ($result) { + dol_syslog("EPCQR Hook: Bilder erfolgreich in ODT eingefügt", LOG_INFO); + } else { + dol_syslog("EPCQR Hook: Fehler beim Einfügen der Bilder", LOG_WARNING); + } + + return 0; + } + + /** + * Hook für PDF-Generierung (falls später benötigt) + * + * @param array $parameters Hook-Parameter + * @param object $object Dolibarr-Objekt + * @param string $action Aktuelle Aktion + * @return int 0 = OK, <0 = Fehler + */ + public function beforePDFCreation($parameters, &$object, &$action) + { + // Hier könnte später Code für PDF-Verarbeitung eingefügt werden + return 0; + } + + /** + * Hook für completesubstitutionarray + * Fügt zusätzliche Substitutionswerte hinzu + * + * @param array $parameters Hook-Parameter + * @param object $object Dolibarr-Objekt + * @param string $action Aktuelle Aktion + * @return int 0 = OK, <0 = Fehler + */ + public function completesubstitutionarray($parameters, &$object, &$action) + { + dol_syslog("EPCQR Hook: completesubstitutionarray aufgerufen", LOG_DEBUG); + + if (empty($parameters['substitutionarray'])) { + dol_syslog("EPCQR Hook: Kein substitutionarray in Parametern", LOG_DEBUG); + return 0; + } + + // Substitutionsfunktion aufrufen + require_once __DIR__.'/../core/substitutions/functions_epcqr.lib.php'; + epcqr_completesubstitutionarray( + $parameters['substitutionarray'], + $this->langs, + $object, + isset($parameters['outputlangs']) ? $parameters['outputlangs'] : null + ); + + return 0; + } +} diff --git a/core/modules/modEpcqr.class.php b/core/modules/modEpcqr.class.php new file mode 100755 index 0000000..643a8c2 --- /dev/null +++ b/core/modules/modEpcqr.class.php @@ -0,0 +1,543 @@ + + * Copyright (C) 2018-2019 Nicolas ZABOURI + * Copyright (C) 2019-2024 Frédéric France + * Copyright (C) 2025 Eduard Wisch + * + * 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 . + */ + +/** + * \defgroup epcqr Module Epcqr + * \brief Epcqr module descriptor. + * + * \file htdocs/epcqr/core/modules/modEpcqr.class.php + * \ingroup epcqr + * \brief Description and activation file for module Epcqr + */ +include_once DOL_DOCUMENT_ROOT.'/core/modules/DolibarrModules.class.php'; + + +/** + * Description and activation class for module Epcqr + */ +class modEpcqr extends DolibarrModules +{ + /** + * Constructor. Define names, constants, directories, boxes, permissions + * + * @param DoliDB $db Database handler + */ + public function __construct($db) + { + global $conf, $langs; + + $this->db = $db; + + // Id for module (must be unique). + // Use here a free id (See in Home -> System information -> Dolibarr for list of used modules id). + $this->numero = 500000; // TODO Go on page https://wiki.dolibarr.org/index.php/List_of_modules_id to reserve an id number for your module + + // Key text used to identify module (for permissions, menus, etc...) + $this->rights_class = 'epcqr'; + + // Family can be 'base' (core modules),'crm','financial','hr','projects','products','ecm','technic' (transverse modules),'interface' (link with external tools),'other','...' + // It is used to group modules by family in module setup page + $this->family = 'financial'; + + // Module position in the family on 2 digits ('01', '10', '20', ...) + $this->module_position = '90'; + + // Gives the possibility for the module, to provide his own family info and position of this family (Overwrite $this->family and $this->module_position. Avoid this) + //$this->familyinfo = array('myownfamily' => array('position' => '01', 'label' => $langs->trans("MyOwnFamily"))); + // Module label (no space allowed), used if translation string 'ModuleEpcqrName' not found (Epcqr is name of module). + $this->name = preg_replace('/^mod/i', '', get_class($this)); + + // DESCRIPTION_FLAG + // Module description, used if translation string 'ModuleEpcqrDesc' not found (Epcqr is name of module). + $this->description = 'QRCode wird generiert und als Bild sowie URL in den Extra Feldern qrcode qrcodepfad hinterlegt'; + // Used only if file README.md and README-LL.md not found. + $this->descriptionlong = "EpcqrDescription"; + + // Author + $this->editor_name = 'Alles Watt läuft'; + $this->editor_url = ''; // Must be an external online web site + $this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@epcqr' + + // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z' + $this->version = '1.5'; + // Url to the file with your last numberversion of this module + //$this->url_last_version = 'http://www.example.com/versionmodule.txt'; + + // Key used in llx_const table to save module status enabled/disabled (where EPCQR is value of property name of module in uppercase) + $this->const_name = 'MAIN_MODULE_'.strtoupper($this->name); + + // Name of image file used for this module. + // If file is in theme/yourtheme/img directory under name object_pictovalue.png, use this->picto='pictovalue' + // If file is in module/img directory under name object_pictovalue.png, use this->picto='pictovalue@module' + // To use a supported fa-xxx css style of font awesome, use this->picto='xxx' + $this->picto = 'fa-file'; + + // Define some features supported by module (triggers, login, substitutions, menus, css, etc...) + $this->module_parts = array( + // Set this to 1 if module has its own trigger directory (core/triggers) + 'triggers' => 1, + // Set this to 1 if module has its own login method file (core/login) + 'login' => 0, + // Set this to 1 if module has its own substitution function file (core/substitutions) + 'substitutions' => 1, + // Set this to 1 if module has its own menus handler directory (core/menus) + 'menus' => 0, + // Set this to 1 if module overwrite template dir (core/tpl) + 'tpl' => 0, + // Set this to 1 if module has its own barcode directory (core/modules/barcode) + 'barcode' => 0, + // Set this to 1 if module has its own models directory (core/modules/xxx) + 'models' => 0, + // Set this to 1 if module has its own printing directory (core/modules/printing) + 'printing' => 0, + // Set this to 1 if module has its own theme directory (theme) + 'theme' => 0, + // Set this to relative path of css file if module has its own css file + 'css' => array( + // '/epcqr/css/epcqr.css.php', + ), + // Set this to relative path of js file if module must load a js on all pages + 'js' => array( + // '/epcqr/js/epcqr.js.php', + ), + // Set here all hooks context managed by module. To find available hook context, make a "grep -r '>initHooks(' *" on source code. You can also set hook context to 'all' + /* BEGIN MODULEBUILDER HOOKSCONTEXTS */ + 'hooks' => array( + 'data' => array( + 'pdfgeneration', + 'odtgeneration', + 'invoicecard', + 'propalcard', + ), + 'entity' => '0', + ), + /* END MODULEBUILDER HOOKSCONTEXTS */ + // Set this to 1 if features of module are opened to external users + 'moduleforexternal' => 0, + // Set this to 1 if the module provides a website template into doctemplates/websites/website_template-mytemplate + 'websitetemplates' => 0, + // Set this to 1 if the module provides a captcha driver + 'captcha' => 0 + ); + + // Data directories to create when module is enabled. + // Example: this->dirs = array("/epcqr/temp","/epcqr/subdir"); + $this->dirs = array("/epcqr/temp"); + + // Config pages. Put here list of php page, stored into epcqr/admin directory, to use to setup module. + $this->config_page_url = array("setup.php@epcqr"); + + // Dependencies + // A condition to hide module + $this->hidden = getDolGlobalInt('MODULE_EPCQR_DISABLED'); // A condition to disable module; + // List of module class names that must be enabled if this module is enabled. Example: array('always'=>array('modModuleToEnable1','modModuleToEnable2'), 'FR'=>array('modModuleToEnableFR')...) + $this->depends = array(); + // List of module class names to disable if this one is disabled. Example: array('modModuleToDisable1', ...) + $this->requiredby = array(); + // List of module class names this module is in conflict with. Example: array('modModuleToDisable1', ...) + $this->conflictwith = array(); + + // The language file dedicated to your module + $this->langfiles = array("epcqr@epcqr"); + + // Prerequisites + $this->phpmin = array(7, 1); // Minimum version of PHP required by module + // $this->phpmax = array(8, 0); // Maximum version of PHP required by module + $this->need_dolibarr_version = array(19, -3); // Minimum version of Dolibarr required by module + // $this->max_dolibarr_version = array(19, -3); // Maximum version of Dolibarr required by module + $this->need_javascript_ajax = 0; + + // Messages at activation + $this->warnings_activation = array(); // Warning to show when we activate module. array('always'='text') or array('FR'='textfr','MX'='textmx'...) + $this->warnings_activation_ext = array(); // Warning to show when we activate an external module. array('always'='text') or array('FR'='textfr','MX'='textmx'...) + //$this->automatic_activation = array('FR'=>'EpcqrWasAutomaticallyActivatedBecauseOfYourCountryChoice'); + //$this->always_enabled = true; // If true, can't be disabled + + // Constants + // List of particular constants to add when module is enabled (key, 'chaine', value, desc, visible, 'current' or 'allentities', deleteonunactive) + // Example: $this->const=array(1 => array('EPCQR_MYNEWCONST1', 'chaine', 'myvalue', 'This is a constant to add', 1), + // 2 => array('EPCQR_MYNEWCONST2', 'chaine', 'myvalue', 'This is another constant to add', 0, 'current', 1) + // ); + $this->const = array(); + + // Some keys to add into the overwriting translation tables + /*$this->overwrite_translation = array( + 'en_US:ParentCompany'=>'Parent company or reseller', + 'fr_FR:ParentCompany'=>'Maison mère ou revendeur' + )*/ + + if (!isModEnabled("epcqr")) { + $conf->epcqr = new stdClass(); + $conf->epcqr->enabled = 0; + } + + // Array to add new pages in new tabs + /* BEGIN MODULEBUILDER TABS */ + $this->tabs = array(); + /* END MODULEBUILDER TABS */ + // Example: + // To add a new tab identified by code tabname1 + // $this->tabs[] = array('data' => 'objecttype:+tabname1:Title1:mylangfile@epcqr:$user->hasRight(\'epcqr\', \'read\'):/epcqr/mynewtab1.php?id=__ID__'); + // To add another new tab identified by code tabname2. Label will be result of calling all substitution functions on 'Title2' key. + // $this->tabs[] = array('data' => 'objecttype:+tabname2:SUBSTITUTION_Title2:mylangfile@epcqr:$user->hasRight(\'othermodule\', \'read\'):/epcqr/mynewtab2.php?id=__ID__', + // To remove an existing tab identified by code tabname + // $this->tabs[] = array('data' => 'objecttype:-tabname:NU:conditiontoremove'); + // + // Where objecttype can be + // 'categories_x' to add a tab in category view (replace 'x' by type of category (0=product, 1=supplier, 2=customer, 3=member) + // 'contact' to add a tab in contact view + // 'contract' to add a tab in contract view + // 'delivery' to add a tab in delivery view + // 'group' to add a tab in group view + // 'intervention' to add a tab in intervention view + // 'invoice' to add a tab in customer invoice view + // 'invoice_supplier' to add a tab in supplier invoice view + // 'member' to add a tab in foundation member view + // 'opensurveypoll' to add a tab in opensurvey poll view + // 'order' to add a tab in sale order view + // 'order_supplier' to add a tab in supplier order view + // 'payment' to add a tab in payment view + // 'payment_supplier' to add a tab in supplier payment view + // 'product' to add a tab in product view + // 'propal' to add a tab in propal view + // 'project' to add a tab in project view + // 'stock' to add a tab in stock view + // 'thirdparty' to add a tab in third party view + // 'user' to add a tab in user view + + + // Dictionaries + /* Example: + $this->dictionaries=array( + 'langs' => 'epcqr@epcqr', + // List of tables we want to see into dictionary editor + 'tabname' => array("table1", "table2", "table3"), + // Label of tables + 'tablib' => array("Table1", "Table2", "Table3"), + // Request to select fields + 'tabsql' => array('SELECT f.rowid as rowid, f.code, f.label, f.active FROM '.$this->db->prefix().'table1 as f', 'SELECT f.rowid as rowid, f.code, f.label, f.active FROM '.$this->db->prefix().'table2 as f', 'SELECT f.rowid as rowid, f.code, f.label, f.active FROM '.$this->db->prefix().'table3 as f'), + // Sort order + 'tabsqlsort' => array("label ASC", "label ASC", "label ASC"), + // List of fields (result of select to show dictionary) + 'tabfield' => array("code,label", "code,label", "code,label"), + // List of fields (list of fields to edit a record) + 'tabfieldvalue' => array("code,label", "code,label", "code,label"), + // List of fields (list of fields for insert) + 'tabfieldinsert' => array("code,label", "code,label", "code,label"), + // Name of columns with primary key (try to always name it 'rowid') + 'tabrowid' => array("rowid", "rowid", "rowid"), + // Condition to show each dictionary + 'tabcond' => array(isModEnabled('epcqr'), isModEnabled('epcqr'), isModEnabled('epcqr')), + // Tooltip for every fields of dictionaries: DO NOT PUT AN EMPTY ARRAY + 'tabhelp' => array(array('code' => $langs->trans('CodeTooltipHelp'), 'field2' => 'field2tooltip'), array('code' => $langs->trans('CodeTooltipHelp'), 'field2' => 'field2tooltip'), ...), + ); + */ + /* BEGIN MODULEBUILDER DICTIONARIES */ + $this->dictionaries = array(); + /* END MODULEBUILDER DICTIONARIES */ + + // Boxes/Widgets + // Add here list of php file(s) stored in epcqr/core/boxes that contains a class to show a widget. + /* BEGIN MODULEBUILDER WIDGETS */ + $this->boxes = array( + // 0 => array( + // 'file' => 'epcqrwidget1.php@epcqr', + // 'note' => 'Widget provided by Epcqr', + // 'enabledbydefaulton' => 'Home', + // ), + // ... + ); + /* END MODULEBUILDER WIDGETS */ + + // Cronjobs (List of cron jobs entries to add when module is enabled) + // unit_frequency must be 60 for minute, 3600 for hour, 86400 for day, 604800 for week + /* BEGIN MODULEBUILDER CRON */ + $this->cronjobs = array( + // 0 => array( + // 'label' => 'MyJob label', + // 'jobtype' => 'method', + // 'class' => '/epcqr/class/myobject.class.php', + // 'objectname' => 'MyObject', + // 'method' => 'doScheduledJob', + // 'parameters' => '', + // 'comment' => 'Comment', + // 'frequency' => 2, + // 'unitfrequency' => 3600, + // 'status' => 0, + // 'test' => 'isModEnabled("epcqr")', + // 'priority' => 50, + // ), + ); + /* END MODULEBUILDER CRON */ + // Example: $this->cronjobs=array( + // 0=>array('label'=>'My label', 'jobtype'=>'method', 'class'=>'/dir/class/file.class.php', 'objectname'=>'MyClass', 'method'=>'myMethod', 'parameters'=>'param1, param2', 'comment'=>'Comment', 'frequency'=>2, 'unitfrequency'=>3600, 'status'=>0, 'test'=>'isModEnabled("epcqr")', 'priority'=>50), + // 1=>array('label'=>'My label', 'jobtype'=>'command', 'command'=>'', 'parameters'=>'param1, param2', 'comment'=>'Comment', 'frequency'=>1, 'unitfrequency'=>3600*24, 'status'=>0, 'test'=>'isModEnabled("epcqr")', 'priority'=>50) + // ); + + // Permissions provided by this module + $this->rights = array(); + $r = 0; + // Add here entries to declare new permissions + /* BEGIN MODULEBUILDER PERMISSIONS */ + /* + $o = 1; + $this->rights[$r][0] = $this->numero . sprintf("%02d", ($o * 10) + 1); // Permission id (must not be already used) + $this->rights[$r][1] = 'Read objects of Epcqr'; // Permission label + $this->rights[$r][4] = 'myobject'; + $this->rights[$r][5] = 'read'; // In php code, permission will be checked by test if ($user->hasRight('epcqr', 'myobject', 'read')) + $r++; + $this->rights[$r][0] = $this->numero . sprintf("%02d", ($o * 10) + 2); // Permission id (must not be already used) + $this->rights[$r][1] = 'Create/Update objects of Epcqr'; // Permission label + $this->rights[$r][4] = 'myobject'; + $this->rights[$r][5] = 'write'; // In php code, permission will be checked by test if ($user->hasRight('epcqr', 'myobject', 'write')) + $r++; + $this->rights[$r][0] = $this->numero . sprintf("%02d", ($o * 10) + 3); // Permission id (must not be already used) + $this->rights[$r][1] = 'Delete objects of Epcqr'; // Permission label + $this->rights[$r][4] = 'myobject'; + $this->rights[$r][5] = 'delete'; // In php code, permission will be checked by test if ($user->hasRight('epcqr', 'myobject', 'delete')) + $r++; + */ + /* END MODULEBUILDER PERMISSIONS */ + + + // Main menu entries to add + $this->menu = array(); + $r = 0; + // Add here entries to declare new menus + /* BEGIN MODULEBUILDER TOPMENU */ + /*$this->menu[$r++] = array( + 'fk_menu' => '', // Will be stored into mainmenu + leftmenu. Use '' if this is a top menu. For left menu, use 'fk_mainmenu=xxx' or 'fk_mainmenu=xxx,fk_leftmenu=yyy' where xxx is mainmenucode and yyy is a leftmenucode + 'type' => 'top', // This is a Top menu entry + 'titre' => 'ModuleEpcqrName', + 'prefix' => img_picto('', $this->picto, 'class="pictofixedwidth valignmiddle"'), + 'mainmenu' => 'epcqr', + 'leftmenu' => '', + 'url' => '/epcqr/epcqrindex.php', + 'langs' => 'epcqr@epcqr', // Lang file to use (without .lang) by module. File must be in langs/code_CODE/ directory. + 'position' => 1000 + $r, + 'enabled' => 'isModEnabled("epcqr")', // Define condition to show or hide menu entry. Use 'isModEnabled("epcqr")' if entry must be visible if module is enabled. + 'perms' => '1', // Use 'perms'=>'$user->hasRight("epcqr", "myobject", "read")' if you want your menu with a permission rules + 'target' => '', + 'user' => 2, // 0=Menu for internal users, 1=external users, 2=both + )*/ + /* END MODULEBUILDER TOPMENU */ + + /* BEGIN MODULEBUILDER LEFTMENU MYOBJECT */ + /* + $this->menu[$r++]=array( + 'fk_menu' => 'fk_mainmenu=epcqr', // '' if this is a top menu. For left menu, use 'fk_mainmenu=xxx' or 'fk_mainmenu=xxx,fk_leftmenu=yyy' where xxx is mainmenucode and yyy is a leftmenucode + 'type' => 'left', // This is a Left menu entry + 'titre' => 'MyObject', + 'prefix' => img_picto('', $this->picto, 'class="pictofixedwidth valignmiddle paddingright"'), + 'mainmenu' => 'epcqr', + 'leftmenu' => 'myobject', + 'url' => '/epcqr/epcqrindex.php', + 'langs' => 'epcqr@epcqr', // Lang file to use (without .lang) by module. File must be in langs/code_CODE/ directory. + 'position' => 1000 + $r, + 'enabled' => 'isModEnabled("epcqr")', // Define condition to show or hide menu entry. Use 'isModEnabled("epcqr")' if entry must be visible if module is enabled. + 'perms' => '$user->hasRight("epcqr", "myobject", "read")', + 'target' => '', + 'user' => 2, // 0=Menu for internal users, 1=external users, 2=both + 'object' => 'MyObject' + ); + $this->menu[$r++]=array( + 'fk_menu' => 'fk_mainmenu=epcqr,fk_leftmenu=myobject', // '' if this is a top menu. For left menu, use 'fk_mainmenu=xxx' or 'fk_mainmenu=xxx,fk_leftmenu=yyy' where xxx is mainmenucode and yyy is a leftmenucode + 'type' => 'left', // This is a Left menu entry + 'titre' => 'New_MyObject', + 'mainmenu' => 'epcqr', + 'leftmenu' => 'epcqr_myobject_new', + 'url' => '/epcqr/myobject_card.php?action=create', + 'langs' => 'epcqr@epcqr', // Lang file to use (without .lang) by module. File must be in langs/code_CODE/ directory. + 'position' => 1000 + $r, + 'enabled' => 'isModEnabled("epcqr")', // Define condition to show or hide menu entry. Use 'isModEnabled("epcqr")' if entry must be visible if module is enabled. Use '$leftmenu==\'system\'' to show if leftmenu system is selected. + 'perms' => '$user->hasRight("epcqr", "myobject", "write")' + 'target' => '', + 'user' => 2, // 0=Menu for internal users, 1=external users, 2=both + 'object' => 'MyObject' + ); + $this->menu[$r++]=array( + 'fk_menu' => 'fk_mainmenu=epcqr,fk_leftmenu=myobject', // '' if this is a top menu. For left menu, use 'fk_mainmenu=xxx' or 'fk_mainmenu=xxx,fk_leftmenu=yyy' where xxx is mainmenucode and yyy is a leftmenucode + 'type' => 'left', // This is a Left menu entry + 'titre' => 'List_MyObject', + 'mainmenu' => 'epcqr', + 'leftmenu' => 'epcqr_myobject_list', + 'url' => '/epcqr/myobject_list.php', + 'langs' => 'epcqr@epcqr', // Lang file to use (without .lang) by module. File must be in langs/code_CODE/ directory. + 'position' => 1000 + $r, + 'enabled' => 'isModEnabled("epcqr")', // Define condition to show or hide menu entry. Use 'isModEnabled("epcqr")' if entry must be visible if module is enabled. + 'perms' => '$user->hasRight("epcqr", "myobject", "read")' + 'target' => '', + 'user' => 2, // 0=Menu for internal users, 1=external users, 2=both + 'object' => 'MyObject' + ); + */ + /* END MODULEBUILDER LEFTMENU MYOBJECT */ + + + // Exports profiles provided by this module + $r = 0; + /* BEGIN MODULEBUILDER EXPORT MYOBJECT */ + /* + $langs->load("epcqr@epcqr"); + $this->export_code[$r] = $this->rights_class.'_'.$r; + $this->export_label[$r] = 'MyObjectLines'; // Translation key (used only if key ExportDataset_xxx_z not found) + $this->export_icon[$r] = $this->picto; + // Define $this->export_fields_array, $this->export_TypeFields_array and $this->export_entities_array + $keyforclass = 'MyObject'; $keyforclassfile='/epcqr/class/myobject.class.php'; $keyforelement='myobject@epcqr'; + include DOL_DOCUMENT_ROOT.'/core/commonfieldsinexport.inc.php'; + //$this->export_fields_array[$r]['t.fieldtoadd']='FieldToAdd'; $this->export_TypeFields_array[$r]['t.fieldtoadd']='Text'; + //unset($this->export_fields_array[$r]['t.fieldtoremove']); + //$keyforclass = 'MyObjectLine'; $keyforclassfile='/epcqr/class/myobject.class.php'; $keyforelement='myobjectline@epcqr'; $keyforalias='tl'; + //include DOL_DOCUMENT_ROOT.'/core/commonfieldsinexport.inc.php'; + $keyforselect='myobject'; $keyforaliasextra='extra'; $keyforelement='myobject@epcqr'; + include DOL_DOCUMENT_ROOT.'/core/extrafieldsinexport.inc.php'; + //$keyforselect='myobjectline'; $keyforaliasextra='extraline'; $keyforelement='myobjectline@epcqr'; + //include DOL_DOCUMENT_ROOT.'/core/extrafieldsinexport.inc.php'; + //$this->export_dependencies_array[$r] = array('myobjectline' => array('tl.rowid','tl.ref')); // To force to activate one or several fields if we select some fields that need same (like to select a unique key if we ask a field of a child to avoid the DISTINCT to discard them, or for computed field than need several other fields) + //$this->export_special_array[$r] = array('t.field' => '...'); + //$this->export_examplevalues_array[$r] = array('t.field' => 'Example'); + //$this->export_help_array[$r] = array('t.field' => 'FieldDescHelp'); + $this->export_sql_start[$r]='SELECT DISTINCT '; + $this->export_sql_end[$r] =' FROM '.$this->db->prefix().'epcqr_myobject as t'; + //$this->export_sql_end[$r] .=' LEFT JOIN '.$this->db->prefix().'epcqr_myobject_line as tl ON tl.fk_myobject = t.rowid'; + $this->export_sql_end[$r] .=' WHERE 1 = 1'; + $this->export_sql_end[$r] .=' AND t.entity IN ('.getEntity('myobject').')'; + $r++; */ + /* END MODULEBUILDER EXPORT MYOBJECT */ + + // Imports profiles provided by this module + $r = 0; + /* BEGIN MODULEBUILDER IMPORT MYOBJECT */ + /* + $langs->load("epcqr@epcqr"); + $this->import_code[$r] = $this->rights_class.'_'.$r; + $this->import_label[$r] = 'MyObjectLines'; // Translation key (used only if key ExportDataset_xxx_z not found) + $this->import_icon[$r] = $this->picto; + $this->import_tables_array[$r] = array('t' => $this->db->prefix().'epcqr_myobject', 'extra' => $this->db->prefix().'epcqr_myobject_extrafields'); + $this->import_tables_creator_array[$r] = array('t' => 'fk_user_author'); // Fields to store import user id + $import_sample = array(); + $keyforclass = 'MyObject'; $keyforclassfile='/epcqr/class/myobject.class.php'; $keyforelement='myobject@epcqr'; + include DOL_DOCUMENT_ROOT.'/core/commonfieldsinimport.inc.php'; + $import_extrafield_sample = array(); + $keyforselect='myobject'; $keyforaliasextra='extra'; $keyforelement='myobject@epcqr'; + include DOL_DOCUMENT_ROOT.'/core/extrafieldsinimport.inc.php'; + $this->import_fieldshidden_array[$r] = array('extra.fk_object' => 'lastrowid-'.$this->db->prefix().'epcqr_myobject'); + $this->import_regex_array[$r] = array(); + $this->import_examplevalues_array[$r] = array_merge($import_sample, $import_extrafield_sample); + $this->import_updatekeys_array[$r] = array('t.ref' => 'Ref'); + $this->import_convertvalue_array[$r] = array( + 't.ref' => array( + 'rule'=>'getrefifauto', + 'class'=>(!getDolGlobalString('EPCQR_MYOBJECT_ADDON') ? 'mod_myobject_standard' : getDolGlobalString('EPCQR_MYOBJECT_ADDON')), + 'path'=>"/core/modules/epcqr/".(!getDolGlobalString('EPCQR_MYOBJECT_ADDON') ? 'mod_myobject_standard' : getDolGlobalString('EPCQR_MYOBJECT_ADDON')).'.php', + 'classobject'=>'MyObject', + 'pathobject'=>'/epcqr/class/myobject.class.php', + ), + 't.fk_soc' => array('rule' => 'fetchidfromref', 'file' => '/societe/class/societe.class.php', 'class' => 'Societe', 'method' => 'fetch', 'element' => 'ThirdParty'), + 't.fk_user_valid' => array('rule' => 'fetchidfromref', 'file' => '/user/class/user.class.php', 'class' => 'User', 'method' => 'fetch', 'element' => 'user'), + 't.fk_mode_reglement' => array('rule' => 'fetchidfromcodeorlabel', 'file' => '/compta/paiement/class/cpaiement.class.php', 'class' => 'Cpaiement', 'method' => 'fetch', 'element' => 'cpayment'), + ); + $this->import_run_sql_after_array[$r] = array(); + $r++; */ + /* END MODULEBUILDER IMPORT MYOBJECT */ + } + + /** + * Function called when module is enabled. + * The init function add constants, boxes, permissions and menus (defined in constructor) into Dolibarr database. + * It also creates data directories + * + * @param string $options Options when enabling module ('', 'noboxes') + * @return int<-1,1> 1 if OK, <=0 if KO + */ + public function init($options = '') + { + global $conf, $langs; + + // Create tables of module at module activation + //$result = $this->_load_tables('/install/mysql/', 'epcqr'); + $result = $this->_load_tables('/epcqr/sql/'); + if ($result < 0) { + return -1; // Do not activate module if error 'not allowed' returned when loading module SQL queries (the _load_table run sql with run_sql with the error allowed parameter set to 'default') + } + + // Create extrafields during init + //include_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php'; + //$extrafields = new ExtraFields($this->db); + //$result0=$extrafields->addExtraField('epcqr_separator1', "Separator 1", 'separator', 1, 0, 'thirdparty', 0, 0, '', array('options'=>array(1=>1)), 1, '', 1, 0, '', '', 'epcqr@epcqr', 'isModEnabled("epcqr")'); + //$result1=$extrafields->addExtraField('epcqr_myattr1', "New Attr 1 label", 'boolean', 1, 3, 'thirdparty', 0, 0, '', '', 1, '', -1, 0, '', '', 'epcqr@epcqr', 'isModEnabled("epcqr")'); + //$result2=$extrafields->addExtraField('epcqr_myattr2', "New Attr 2 label", 'varchar', 1, 10, 'project', 0, 0, '', '', 1, '', -1, 0, '', '', 'epcqr@epcqr', 'isModEnabled("epcqr")'); + //$result3=$extrafields->addExtraField('epcqr_myattr3', "New Attr 3 label", 'varchar', 1, 10, 'bank_account', 0, 0, '', '', 1, '', -1, 0, '', '', 'epcqr@epcqr', 'isModEnabled("epcqr")'); + //$result4=$extrafields->addExtraField('epcqr_myattr4', "New Attr 4 label", 'select', 1, 3, 'thirdparty', 0, 1, '', array('options'=>array('code1'=>'Val1','code2'=>'Val2','code3'=>'Val3')), 1,'', -1, 0, '', '', 'epcqr@epcqr', 'isModEnabled("epcqr")'); + //$result5=$extrafields->addExtraField('epcqr_myattr5', "New Attr 5 label", 'text', 1, 10, 'user', 0, 0, '', '', 1, '', -1, 0, '', '', 'epcqr@epcqr', 'isModEnabled("epcqr")'); + + // Permissions + $this->remove($options); + + $sql = array(); + + // Document templates + $moduledir = dol_sanitizeFileName('epcqr'); + $myTmpObjects = array(); + $myTmpObjects['MyObject'] = array('includerefgeneration' => 0, 'includedocgeneration' => 0); + + foreach ($myTmpObjects as $myTmpObjectKey => $myTmpObjectArray) { + if ($myTmpObjectArray['includerefgeneration']) { + $src = DOL_DOCUMENT_ROOT.'/install/doctemplates/'.$moduledir.'/template_myobjects.odt'; + $dirodt = DOL_DATA_ROOT.($conf->entity > 1 ? '/'.$conf->entity : '').'/doctemplates/'.$moduledir; + $dest = $dirodt.'/template_myobjects.odt'; + + if (file_exists($src) && !file_exists($dest)) { + require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; + dol_mkdir($dirodt); + $result = dol_copy($src, $dest, '0', 0); + if ($result < 0) { + $langs->load("errors"); + $this->error = $langs->trans('ErrorFailToCopyFile', $src, $dest); + return 0; + } + } + + $sql = array_merge($sql, array( + "DELETE FROM ".$this->db->prefix()."document_model WHERE nom = 'standard_".strtolower($myTmpObjectKey)."' AND type = '".$this->db->escape(strtolower($myTmpObjectKey))."' AND entity = ".((int) $conf->entity), + "INSERT INTO ".$this->db->prefix()."document_model (nom, type, entity) VALUES('standard_".strtolower($myTmpObjectKey)."', '".$this->db->escape(strtolower($myTmpObjectKey))."', ".((int) $conf->entity).")", + "DELETE FROM ".$this->db->prefix()."document_model WHERE nom = 'generic_".strtolower($myTmpObjectKey)."_odt' AND type = '".$this->db->escape(strtolower($myTmpObjectKey))."' AND entity = ".((int) $conf->entity), + "INSERT INTO ".$this->db->prefix()."document_model (nom, type, entity) VALUES('generic_".strtolower($myTmpObjectKey)."_odt', '".$this->db->escape(strtolower($myTmpObjectKey))."', ".((int) $conf->entity).")" + )); + } + } + + return $this->_init($sql, $options); + } + + /** + * Function called when module is disabled. + * Remove from database constants, boxes and permissions from Dolibarr database. + * Data directories are not deleted + * + * @param string $options Options when enabling module ('', 'noboxes') + * @return int<-1,1> 1 if OK, <=0 if KO + */ + public function remove($options = '') + { + $sql = array(); + return $this->_remove($sql, $options); + } +} diff --git a/core/substitutions/functions_epcqr.lib.php b/core/substitutions/functions_epcqr.lib.php new file mode 100644 index 0000000..f9adf73 --- /dev/null +++ b/core/substitutions/functions_epcqr.lib.php @@ -0,0 +1,274 @@ + + * + * 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 . + */ + +/** + * \file core/substitutions/functions_epcqr.lib.php + * \ingroup epcqr + * \brief Substitutionsfunktionen für QR-Codes und Bilder in Dokumenten + */ + +/** + * Füllt Substitutionsarray mit Bildpfaden und QR-Code-Daten + * + * @param array &$substitutionarray Array mit Substitutionswerten + * @param Translate $langs Sprachobjekt + * @param CommonObject $object Dolibarr-Objekt (Rechnung, Angebot, etc.) + * @param string $outputlangs Ausgabesprache + * @return void + */ +function epcqr_completesubstitutionarray(&$substitutionarray, $langs, $object, $outputlangs = null) +{ + global $conf, $db; + + dol_syslog("EPCQR: START completesubstitutionarray für ".get_class($object), LOG_DEBUG); + + // Prüfen ob Objekt gültig ist + if (!is_object($object) || empty($object->id)) { + dol_syslog("EPCQR: Object ist null oder hat keine ID - überspringe", LOG_DEBUG); + return; + } + + // QR-Code Pfad aus Extra-Feldern + $qrCodePath = ''; + + // Prüfen ob Extra-Felder vorhanden sind + if (isset($object->array_options['options_qrcodepath'])) { + $qrCodePath = $object->array_options['options_qrcodepath']; + dol_syslog("EPCQR: QR-Code Pfad gefunden: ".$qrCodePath, LOG_DEBUG); + } + + // Substitution für QR-Code-Bildpfad + $substitutionarray['qrcode_path'] = $qrCodePath; + + // Für ODT-Dokumente: Spezielle Marker für Bildoperationen + // Diese werden später von der ODT-Erweiterung verarbeitet + $substitutionarray['__IMAGE_qrcode__'] = $qrCodePath; + + // Allgemeine Bildfunktion: Jedes Extrafeld das auf "_imagepath" endet + // wird als Bildpfad interpretiert + if (!empty($object->array_options)) { + foreach ($object->array_options as $key => $value) { + if (preg_match('/^options_(.+)_imagepath$/', $key, $matches)) { + $fieldname = $matches[1]; + $substitutionarray['__IMAGE_'.$fieldname.'__'] = $value; + dol_syslog("EPCQR: Bildpfad gefunden für ".$fieldname.": ".$value, LOG_DEBUG); + } + } + } + + dol_syslog("EPCQR: ENDE completesubstitutionarray", LOG_DEBUG); +} + +/** + * Fügt Bilder in ODT-Dokumente ein + * + * Diese Funktion wird nach der ODT-Generierung aufgerufen und + * ersetzt Bild-Marker durch tatsächliche Bilder im ODT + * + * @param string $odfFilePath Pfad zur generierten ODT-Datei + * @param array $imageData Array mit Bildinformationen + * @return bool true bei Erfolg, false bei Fehler + */ +function epcqr_insertImagesIntoODT($odfFilePath, $imageData) +{ + global $conf; + + dol_syslog("EPCQR: START insertImagesIntoODT für ".$odfFilePath, LOG_DEBUG); + + if (!file_exists($odfFilePath)) { + dol_syslog("EPCQR: ODT-Datei existiert nicht: ".$odfFilePath, LOG_ERR); + return false; + } + + // Temporäres Verzeichnis erstellen + $tmpDir = sys_get_temp_dir() . '/odt_epcqr_' . uniqid(); + if (!mkdir($tmpDir)) { + dol_syslog("EPCQR: Konnte temporäres Verzeichnis nicht erstellen", LOG_ERR); + return false; + } + + try { + // 1. ODT entpacken (ODT ist ZIP) + $zip = new ZipArchive(); + if ($zip->open($odfFilePath) !== true) { + dol_syslog("EPCQR: Konnte ODT nicht öffnen: ".$odfFilePath, LOG_ERR); + epcqr_deleteDir($tmpDir); + return false; + } + $zip->extractTo($tmpDir); + $zip->close(); + + // 2. Pictures-Verzeichnis erstellen falls nicht vorhanden + if (!is_dir($tmpDir.'/Pictures')) { + mkdir($tmpDir.'/Pictures'); + } + + // 3. Bilder kopieren und XML vorbereiten + $manifest = file_get_contents($tmpDir.'/META-INF/manifest.xml'); + $content = file_get_contents($tmpDir.'/content.xml'); + $imageReplacements = array(); + + foreach ($imageData as $marker => $imagePath) { + if (empty($imagePath) || !file_exists($imagePath)) { + dol_syslog("EPCQR: Bild existiert nicht: ".$imagePath." für Marker ".$marker, LOG_WARNING); + continue; + } + + // Bildinfo ermitteln + $imageInfo = @getimagesize($imagePath); + if ($imageInfo === false) { + dol_syslog("EPCQR: Konnte Bildinfo nicht lesen: ".$imagePath, LOG_WARNING); + continue; + } + + $extension = strtolower(pathinfo($imagePath, PATHINFO_EXTENSION)); + $mimeType = $imageInfo['mime']; + $imageWidth = $imageInfo[0]; + $imageHeight = $imageInfo[1]; + + // Eindeutigen Bildnamen generieren + $imageFilename = 'img_'.md5($marker).'.'.$extension; + $imageDest = $tmpDir.'/Pictures/'.$imageFilename; + + // Bild kopieren + if (!copy($imagePath, $imageDest)) { + dol_syslog("EPCQR: Konnte Bild nicht kopieren: ".$imagePath, LOG_ERR); + continue; + } + + // Manifest.xml aktualisieren + if (strpos($manifest, 'Pictures/'.$imageFilename) === false) { + $newEntry = ''; + $manifest = str_replace('', $newEntry."\n", $manifest); + } + + // Bildgröße in cm berechnen (Annahme: 96 DPI) + $widthCm = round(($imageWidth / 96) * 2.54, 2); + $heightCm = round(($imageHeight / 96) * 2.54, 2); + + // Maximale Größe begrenzen + $maxWidth = 6; // cm + $maxHeight = 6; // cm + + if ($widthCm > $maxWidth || $heightCm > $maxHeight) { + $ratio = min($maxWidth / $widthCm, $maxHeight / $heightCm); + $widthCm = round($widthCm * $ratio, 2); + $heightCm = round($heightCm * $ratio, 2); + } + + // ODF draw:frame XML-Element erstellen + $imageXml = '' + .'' + .''; + + $imageReplacements['{'.$marker.'}'] = $imageXml; + + dol_syslog("EPCQR: Bild vorbereitet: ".$marker." -> ".$imageFilename, LOG_DEBUG); + } + + // 4. Marker in content.xml ersetzen + foreach ($imageReplacements as $marker => $xml) { + $content = str_replace($marker, $xml, $content); + } + + // 5. Dateien zurückschreiben + file_put_contents($tmpDir.'/META-INF/manifest.xml', $manifest); + file_put_contents($tmpDir.'/content.xml', $content); + + // 6. Neue ODT erstellen + $zip = new ZipArchive(); + if ($zip->open($odfFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { + dol_syslog("EPCQR: Konnte neue ODT nicht erstellen", LOG_ERR); + epcqr_deleteDir($tmpDir); + return false; + } + + // mimetype MUSS zuerst und unkomprimiert + $zip->addFile($tmpDir.'/mimetype', 'mimetype'); + $zip->setCompressionName('mimetype', ZipArchive::CM_STORE); + + // Restliche Dateien hinzufügen + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($tmpDir, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ($files as $file) { + if (!$file->isFile()) { + continue; + } + $filePath = $file->getRealPath(); + $relativePath = substr($filePath, strlen($tmpDir) + 1); + + // mimetype überspringen (bereits hinzugefügt) + if ($relativePath === 'mimetype') { + continue; + } + + // Backslashes zu Forward-Slashes + $relativePath = str_replace('\\', '/', $relativePath); + + $zip->addFile($filePath, $relativePath); + } + + $zip->close(); + + // 7. Cleanup + epcqr_deleteDir($tmpDir); + + dol_syslog("EPCQR: ENDE insertImagesIntoODT - Erfolgreich", LOG_DEBUG); + return true; + } catch (Exception $e) { + dol_syslog("EPCQR: Exception in insertImagesIntoODT: ".$e->getMessage(), LOG_ERR); + if (is_dir($tmpDir)) { + epcqr_deleteDir($tmpDir); + } + return false; + } +} + +/** + * Hilfsfunktion: Löscht Verzeichnis rekursiv + * + * @param string $dir Verzeichnispfad + * @return void + */ +function epcqr_deleteDir($dir) +{ + if (!is_dir($dir)) { + return; + } + $items = scandir($dir); + foreach ($items as $item) { + if ($item === '.' || $item === '..') { + continue; + } + $path = $dir.'/'.$item; + if (is_dir($path)) { + epcqr_deleteDir($path); + } else { + unlink($path); + } + } + rmdir($dir); +} diff --git a/core/triggers/interface_99_modEpcqr_EpcqrTriggers.class.php b/core/triggers/interface_99_modEpcqr_EpcqrTriggers.class.php new file mode 100755 index 0000000..c9cd78b --- /dev/null +++ b/core/triggers/interface_99_modEpcqr_EpcqrTriggers.class.php @@ -0,0 +1,118 @@ + + * Copyright (C) 2025 Eduard Wisch + * + * 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 . + */ + +/** + * \file core/triggers/interface_99_modEpcqr_EpcqrTriggers.class.php + * \ingroup epcqr + * \brief Example of trigger file. + * + * You can create other triggered files by copying this one. + * - File name should be either: + * - interface_99_modEpcqr_MyTrigger.class.php + * - interface_99_all_MyTrigger.class.php + * - The file must stay in core/triggers + * - The class name must be InterfaceMyTrigger + */ + +require_once DOL_DOCUMENT_ROOT.'/core/triggers/dolibarrtriggers.class.php'; + + +/** + * Class of triggers for Epcqr module + */ +class InterfaceEpcqrTriggers extends DolibarrTriggers +{ + /** + * Constructor + * + * @param DoliDB $db Database handler + */ + public function __construct($db) + { + parent::__construct($db); + $this->family = "demo"; + $this->description = "Epcqr triggers."; + $this->version = self::VERSIONS['dev']; + $this->picto = 'epcqr@epcqr'; + } + + /** + * Function called when a Dolibarr business event is done. + * All functions "runTrigger" are triggered if the file is inside the directory core/triggers + * + * @param string $action Event action code + * @param CommonObject $object Object + * @param User $user Object user + * @param Translate $langs Object langs + * @param Conf $conf Object conf + * @return int Return integer <0 if KO, 0 if no triggered ran, >0 if OK + */ + public function runTrigger($action, $object, User $user, Translate $langs, Conf $conf) + { + if (!isModEnabled('epcqr')) { + return 0; // If module is not enabled, we do nothing + } + + // Put here code you want to execute when a Dolibarr business events occurs. + // Data and type of action are stored into $object and $action + + // You can isolate code for each action in a separate method: this method should be named like the trigger in camelCase. + // For example : COMPANY_CREATE => public function companyCreate($action, $object, User $user, Translate $langs, Conf $conf) + $methodName = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', strtolower($action))))); + $callback = array($this, $methodName); + if (is_callable($callback)) { + dol_syslog( + "Trigger '".$this->name."' for action '$action' launched by ".__FILE__.". id=".$object->id + ); + + return call_user_func($callback, $action, $object, $user, $langs, $conf); + } + + // Or you can execute some code here + switch ($action) { // @phan-suppress-current-line PhanNoopSwitchCases + // Bills - QR-Code Generierung bei Rechnungsvalidierung + case 'BILL_VALIDATE': + + dol_syslog("Trigger '".$this->name."' for action '$action' launched by ".__FILE__.". id=".$object->id); + + // WICHTIG: Objekt neu laden, um finale Rechnungsnummer zu bekommen + require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php'; + $invoice = new Facture($this->db); + $result = $invoice->fetch($object->id); + + if ($result > 0) { + // QR-Code lokal generieren + require_once __DIR__.'/../../lib/epcqr.lib.php'; + $qrResult = epcqr_generateQRCodeForInvoice($invoice, $this->db); + + if ($qrResult) { + dol_syslog("QR-Code erfolgreich generiert für Rechnung ".$invoice->ref, LOG_INFO); + } else { + dol_syslog("Fehler beim Generieren des QR-Codes für Rechnung ".$invoice->ref, LOG_ERR); + // Nicht abbrechen, Rechnung ist trotzdem validiert + } + } else { + dol_syslog("Fehler beim Neuladen der Rechnung", LOG_ERR); + return -1; + } + break; + } + + return 0; + } +} diff --git a/doc/BILDER_IN_ODT.md b/doc/BILDER_IN_ODT.md new file mode 100644 index 0000000..053f2ab --- /dev/null +++ b/doc/BILDER_IN_ODT.md @@ -0,0 +1,363 @@ +# Bilder in ODT-Dokumenten mit EPCQR-Modul + +## Übersicht + +Das EPCQR-Modul bietet eine **wiederverwendbare Lösung** zum Einfügen von Bildern in ODT-Dokumentvorlagen. Die Implementierung ist generisch und kann für beliebige Bilder verwendet werden, nicht nur für QR-Codes. + +## Funktionsweise + +### 1. QR-Code Keyword: `{qrcode}` + +Das neue Keyword `{qrcode}` kann in jedem ODT-Dokumenttemplate verwendet werden und wird automatisch durch ein QR-Code-Bild ersetzt. + +**Beispiel in ODT-Template:** +``` +Rechnung: {ref} +Betrag: {total_ttc} + +Bitte überweisen Sie den Betrag mit folgendem QR-Code: +{qrcode} +``` + +### 2. Generische Bildintegration + +Das System unterstützt auch **beliebige andere Bilder**: + +- Erstellen Sie ein Extrafeld mit dem Suffix `_imagepath` +- Speichern Sie den Dateipfad zum Bild im Extrafeld +- Verwenden Sie das Keyword `{FELDNAME}` im ODT-Template + +**Beispiel:** +1. Extrafeld: `logo_imagepath` (Typ: varchar) +2. Speichern: `/pfad/zum/logo.png` im Extrafeld +3. Im Template: `{logo}` + +## Installation und Konfiguration + +### Schritt 1: Modul aktualisieren + +Das Modul muss deaktiviert und neu aktiviert werden, damit die neuen Funktionen verfügbar sind: + +1. **Home → Setup → Modules/Applications** +2. **EPCQR-Modul deaktivieren** +3. **EPCQR-Modul aktivieren** + +### Schritt 2: Extrafeld erstellen (automatisch) + +Das SQL-Update-Script erstellt automatisch das Extrafeld `qrcodepath`: + +```sql +-- Wird automatisch bei Modulaktivierung ausgeführt +INSERT INTO llx_extrafields (name, label, type, elementtype, ...) +VALUES ('qrcodepath', 'QR-Code Bildpfad (lokal)', 'varchar', 'facture', ...); +``` + +### Schritt 3: Konfiguration anpassen (optional) + +Bankverbindung in [admin/setup.php](admin/setup.php) konfigurieren: + +- **EPCQR_ACCOUNT_HOLDER**: Kontoinhaber Name +- **EPCQR_IBAN**: IBAN +- **EPCQR_BIC**: BIC + +*Aktuell noch hardcoded im Trigger, wird in Version 1.6 konfigurierbar.* + +## Verwendung in ODT-Templates + +### QR-Code in Rechnung einfügen + +1. **ODT-Template bearbeiten** (z.B. `invoice_template.odt`) +2. **Keyword einfügen**: `{qrcode}` an gewünschter Stelle +3. **Template speichern** in Dolibarr +4. **Rechnung validieren** → QR-Code wird automatisch generiert +5. **Dokument generieren** (ODT) → QR-Code wird eingefügt + +### Eigene Bilder einfügen + +**Beispiel: Firmenlogo** + +1. **Extrafeld erstellen**: + - Name: `company_logo_imagepath` + - Typ: `varchar(255)` + - Element: `facture` + +2. **Bild speichern**: + ```php + $invoice->array_options['options_company_logo_imagepath'] = '/pfad/zum/logo.png'; + $invoice->insertExtraFields(); + ``` + +3. **Template verwenden**: + ``` + {company_logo} + ``` + +## Technische Details + +### Architektur + +``` +┌─────────────────────────────────────┐ +│ 1. BILL_VALIDATE Trigger │ +│ - Rechnung validiert │ +│ - QR-Code generieren │ +│ - Pfad in Extrafeld speichern │ +└──────────────┬──────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ 2. ODT-Generierung │ +│ - Template laden │ +│ - Felder ersetzen │ +│ - ODT-Datei erstellen │ +└──────────────┬──────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ 3. afterODTCreation Hook │ +│ - ODT entpacken (ZIP) │ +│ - Bild in Pictures/ kopieren │ +│ - manifest.xml aktualisieren │ +│ - {qrcode} durch │ +│ XML ersetzen │ +│ - ODT neu packen │ +└─────────────────────────────────────┘ +``` + +### Wichtige Dateien + +| Datei | Funktion | +|-------|----------| +| [lib/qrcode.class.php](../lib/qrcode.class.php) | QR-Code-Generator mit Caching | +| [lib/epcqr.lib.php](../lib/epcqr.lib.php) | Hilfsfunktionen für QR-Codes und Bilder | +| [core/substitutions/functions_epcqr.lib.php](../core/substitutions/functions_epcqr.lib.php) | Substitutionsfunktionen für ODT | +| [class/actions_epcqr.class.php](../class/actions_epcqr.class.php) | Hook-Klasse für ODT-Verarbeitung | +| [core/triggers/interface_99_modEpcqr_EpcqrTriggers.class.php](../core/triggers/interface_99_modEpcqr_EpcqrTriggers.class.php) | Trigger für BILL_VALIDATE | + +### Extrafelder + +| Feldname | Typ | Beschreibung | +|----------|-----|--------------| +| `qrcode` | HTML | QR-Code als ``-Tag (Kompatibilität) | +| `qrcodepfad` | varchar | URL zum QR-Code über viewimage.php | +| `qrcodepath` | varchar | **NEU**: Lokaler Dateipfad für ODT-Integration | + +### Bildverarbeitung + +**ODT ist ein ZIP-Archiv:** + +``` +invoice.odt +├── mimetype (unkomprimiert!) +├── META-INF/ +│ └── manifest.xml (Bild-Referenzen) +├── content.xml (Dokument-Inhalt) +├── styles.xml +└── Pictures/ + ├── qrcode.png + └── logo.png +``` + +**Ablauf:** + +1. **ODT entpacken**: `ZipArchive::extractTo()` +2. **Bild kopieren**: `copy($source, $tmpDir.'/Pictures/img.png')` +3. **Manifest aktualisieren**: + ```xml + + ``` +4. **Content.xml anpassen**: + ```xml + + + + ``` +5. **ODT neu packen**: `ZipArchive::addFile()` mit `mimetype` unkomprimiert + +## QR-Code-Generierung + +### Aktueller Stand (Version 1.5) + +QR-Codes werden **lokal generiert** und gecached: + +```php +$qrGen = new QRCodeGenerator($db); +$qrCodePath = $qrGen->generateEPCQRCode( + 'Eduard Wisch', // Kontoinhaber + 'DE70217625500013438147', // IBAN + 'GENODEF1HUM', // BIC + 150.00, // Betrag + 'IN26-0001' // Verwendungszweck +); +// Ergebnis: /srv/http/dolibarr/documents/epcqr/qrcodes/epc_abc123.png +``` + +**Caching:** +- QR-Codes werden im Verzeichnis `documents/epcqr/qrcodes/` gespeichert +- Dateiname basiert auf MD5-Hash der Parameter +- Wiederverwendung bei identischen Parametern + +### Migration von externem Service + +**Alt (bis Version 1.4):** +```php +$qrurl = "https://qr.data-it-solution.de/epc?..."; +$invoice->array_options['options_qrcode'] = ""; +``` + +**Neu (ab Version 1.5):** +```php +$qrCodePath = $qrGen->generateEPCQRCode(...); +$invoice->array_options['options_qrcodepath'] = $qrCodePath; +$invoice->array_options['options_qrcode'] = ""; +``` + +**Kompatibilität:** +- Bestehende Extrafelder bleiben erhalten +- Alte Rechnungen mit extern gehosteten QR-Codes funktionieren weiter +- Neue Rechnungen verwenden lokale QR-Codes + +## Erweiterte Verwendung + +### Eigene QR-Codes generieren + +```php +require_once DOL_DOCUMENT_ROOT.'/custom/epcqr/lib/qrcode.class.php'; + +$qrGen = new QRCodeGenerator($db); + +// Generischer QR-Code +$qrPath = $qrGen->generateQRCode('https://example.com', 'url'); + +// EPC-QR-Code +$qrPath = $qrGen->generateEPCQRCode( + 'Firma GmbH', + 'DE89370400440532013000', + 'COBADEFFXXX', + 99.99, + 'Rechnung 2026-001' +); +``` + +### Cache-Verwaltung + +```php +// Alte QR-Codes löschen (älter als 30 Tage) +$qrGen = new QRCodeGenerator($db); +$deleted = $qrGen->cleanCache(30); +echo "Gelöscht: ".$deleted." QR-Codes"; +``` + +### Bildgröße anpassen + +Die Bildgröße wird automatisch berechnet und an die ODT-Vorgaben angepasst: + +```php +// In functions_epcqr.lib.php, Zeile ~180 +$widthCm = round(($imageWidth / 96) * 2.54, 2); // Pixel → cm (96 DPI) +$heightCm = round(($imageHeight / 96) * 2.54, 2); + +// Maximale Größe begrenzen +$maxWidth = 6; // cm +$maxHeight = 6; // cm +``` + +**Anpassen:** +Bearbeiten Sie `epcqr_insertImagesIntoODT()` in [core/substitutions/functions_epcqr.lib.php](../core/substitutions/functions_epcqr.lib.php#L180) + +## Fehlerbehebung + +### Problem: QR-Code wird nicht angezeigt + +**Ursache 1: Extrafeld fehlt** +```bash +# Prüfen ob Extrafeld existiert +mysql> SELECT name, label FROM llx_extrafields WHERE name = 'qrcodepath'; +``` + +**Lösung:** +```bash +# SQL-Update manuell ausführen +mysql dolibarr < sql/update_1.5.0.sql +``` + +**Ursache 2: Verzeichnis nicht beschreibbar** +```bash +# Prüfen +ls -la /srv/http/dolibarr/documents/epcqr/ + +# Berechtigungen setzen +chmod 755 /srv/http/dolibarr/documents/epcqr/ +chmod 755 /srv/http/dolibarr/documents/epcqr/qrcodes/ +``` + +**Ursache 3: Hook nicht aktiviert** +```bash +# Prüfen +grep -r "hooks.*array" core/modules/modEpcqr.class.php + +# Sollte enthalten: +'hooks' => array( + 'data' => array('pdfgeneration', 'odtgeneration', ...), +``` + +### Problem: Bild wird im ODT nicht korrekt eingebettet + +**Diagnose:** +1. ODT als ZIP öffnen +2. Prüfen ob `Pictures/qrcode.png` existiert +3. Prüfen ob `META-INF/manifest.xml` Eintrag enthält + +**Lösung:** +- `manifest.xml` muss Bild-Referenz enthalten +- `mimetype` muss unkomprimiert sein +- `content.xml` muss `` Element enthalten + +### Debug-Modus aktivieren + +```php +// In lib/epcqr.lib.php oder functions_epcqr.lib.php +dol_syslog("EPCQR: Debug-Info", LOG_DEBUG); + +// Dolibarr syslog Levels: +// LOG_ERR - Fehler +// LOG_WARNING - Warnungen +// LOG_INFO - Informationen +// LOG_DEBUG - Debug-Informationen +``` + +**Logs ansehen:** +```bash +tail -f /srv/http/dolibarr/documents/dolibarr.log | grep EPCQR +``` + +## Roadmap + +### Version 1.6 (geplant) +- [ ] Native PHP-QR-Code-Generierung (ohne externen Service) +- [ ] Konfigurierbare Bankverbindung in Admin-Panel +- [ ] Unterstützung für mehrere Bankkonten +- [ ] Bildgröße per Konfiguration anpassbar + +### Version 1.7 (geplant) +- [ ] Unterstützung für DOCX-Dokumente +- [ ] Batch-Verarbeitung für mehrere Rechnungen +- [ ] QR-Code Vorschau im Rechnungsformular + +## Support und Beiträge + +**Autor:** Eduard Wisch (data@data-it-solution.de) +**Lizenz:** GPL v3 +**GitHub:** (falls vorhanden) + +**Bei Problemen:** +1. Prüfen Sie die Logs: `documents/dolibarr.log` +2. Aktivieren Sie Debug-Modus +3. Erstellen Sie ein Issue mit Logs und Screenshots diff --git a/epcqrindex.php b/epcqrindex.php new file mode 100755 index 0000000..786200d --- /dev/null +++ b/epcqrindex.php @@ -0,0 +1,259 @@ + + * Copyright (C) 2004-2015 Laurent Destailleur + * Copyright (C) 2005-2012 Regis Houssin + * Copyright (C) 2015 Jean-François Ferry + * Copyright (C) 2024 Frédéric France + * Copyright (C) 2025 Eduard Wisch + * + * 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 . + */ + +/** + * \file epcqr/epcqrindex.php + * \ingroup epcqr + * \brief Home page of epcqr top menu + */ + +// Load Dolibarr environment +$res = 0; +// Try main.inc.php into web root known defined into CONTEXT_DOCUMENT_ROOT (not always defined) +if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) { + $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php"; +} +// Try main.inc.php into web root detected using web root calculated from SCRIPT_FILENAME +$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; +$tmp2 = realpath(__FILE__); +$i = strlen($tmp) - 1; +$j = strlen($tmp2) - 1; +while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { + $i--; + $j--; +} +if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) { + $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php"; +} +if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) { + $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php"; +} +// Try main.inc.php using relative path +if (!$res && file_exists("../main.inc.php")) { + $res = @include "../main.inc.php"; +} +if (!$res && file_exists("../../main.inc.php")) { + $res = @include "../../main.inc.php"; +} +if (!$res && file_exists("../../../main.inc.php")) { + $res = @include "../../../main.inc.php"; +} +if (!$res) { + die("Include of main fails"); +} + +require_once DOL_DOCUMENT_ROOT.'/core/class/html.formfile.class.php'; + +/** + * @var Conf $conf + * @var DoliDB $db + * @var HookManager $hookmanager + * @var Translate $langs + * @var User $user + */ + +// Load translation files required by the page +$langs->loadLangs(array("epcqr@epcqr")); + +$action = GETPOST('action', 'aZ09'); + +$now = dol_now(); +$max = getDolGlobalInt('MAIN_SIZE_SHORTLIST_LIMIT', 5); + +// Security check - Protection if external user +$socid = GETPOSTINT('socid'); +if (!empty($user->socid) && $user->socid > 0) { + $action = ''; + $socid = $user->socid; +} + +// Initialize a technical object to manage hooks. Note that conf->hooks_modules contains array +//$hookmanager->initHooks(array($object->element.'index')); + +// Security check (enable the most restrictive one) +//if ($user->socid > 0) accessforbidden(); +//if ($user->socid > 0) $socid = $user->socid; +//if (!isModEnabled('epcqr')) { +// accessforbidden('Module not enabled'); +//} +//if (! $user->hasRight('epcqr', 'myobject', 'read')) { +// accessforbidden(); +//} +//restrictedArea($user, 'epcqr', 0, 'epcqr_myobject', 'myobject', '', 'rowid'); +//if (empty($user->admin)) { +// accessforbidden('Must be admin'); +//} + + +/* + * Actions + */ + +// None + + +/* + * View + */ + +$form = new Form($db); +$formfile = new FormFile($db); + +llxHeader("", $langs->trans("EpcqrArea"), '', '', 0, 0, '', '', '', 'mod-epcqr page-index'); + +print load_fiche_titre($langs->trans("EpcqrArea"), '', 'epcqr.png@epcqr'); + +print '
'; + + +/* BEGIN MODULEBUILDER DRAFT MYOBJECT +// Draft MyObject +if (isModEnabled('epcqr') && $user->hasRight('epcqr', 'read')) { + $langs->load("orders"); + + $sql = "SELECT c.rowid, c.ref, c.ref_client, c.total_ht, c.tva as total_tva, c.total_ttc, s.rowid as socid, s.nom as name, s.client, s.canvas"; + $sql.= ", s.code_client"; + $sql.= " FROM ".$db->prefix()."commande as c"; + $sql.= ", ".$db->prefix()."societe as s"; + $sql.= " WHERE c.fk_soc = s.rowid"; + $sql.= " AND c.fk_statut = 0"; + $sql.= " AND c.entity IN (".getEntity('commande').")"; + if ($socid) $sql.= " AND c.fk_soc = ".((int) $socid); + + $resql = $db->query($sql); + if ($resql) + { + $total = 0; + $num = $db->num_rows($resql); + + print ''; + print ''; + print ''; + + $var = true; + if ($num > 0) + { + $i = 0; + while ($i < $num) + { + + $obj = $db->fetch_object($resql); + print ''; + print ''; + print ''; + $i++; + $total += $obj->total_ttc; + } + if ($total>0) + { + + print '"; + } + } + else + { + + print ''; + } + print "
'.$langs->trans("DraftMyObjects").($num?''.$num.'':'').'
'; + + $myobjectstatic->id=$obj->rowid; + $myobjectstatic->ref=$obj->ref; + $myobjectstatic->ref_client=$obj->ref_client; + $myobjectstatic->total_ht = $obj->total_ht; + $myobjectstatic->total_tva = $obj->total_tva; + $myobjectstatic->total_ttc = $obj->total_ttc; + + print $myobjectstatic->getNomUrl(1); + print ''; + print ''.price($obj->total_ttc).'
'.$langs->trans("Total").''.price($total)."
'.$langs->trans("NoOrder").'

"; + + $db->free($resql); + } + else + { + dol_print_error($db); + } +} +END MODULEBUILDER DRAFT MYOBJECT */ + + +print '
'; + + +/* BEGIN MODULEBUILDER LASTMODIFIED MYOBJECT +// Last modified myobject +if (isModEnabled('epcqr') && $user->hasRight('epcqr', 'read')) { + $sql = "SELECT s.rowid, s.ref, s.label, s.date_creation, s.tms"; + $sql.= " FROM ".$db->prefix()."epcqr_myobject as s"; + $sql.= " WHERE s.entity IN (".getEntity($myobjectstatic->element).")"; + //if ($socid) $sql.= " AND s.rowid = $socid"; + $sql .= " ORDER BY s.tms DESC"; + $sql .= $db->plimit($max, 0); + + $resql = $db->query($sql); + if ($resql) + { + $num = $db->num_rows($resql); + $i = 0; + + print ''; + print ''; + print ''; + print ''; + print ''; + if ($num) + { + while ($i < $num) + { + $objp = $db->fetch_object($resql); + + $myobjectstatic->id=$objp->rowid; + $myobjectstatic->ref=$objp->ref; + $myobjectstatic->label=$objp->label; + $myobjectstatic->status = $objp->status; + + print ''; + print ''; + print '"; + print '"; + print ''; + $i++; + } + + $db->free($resql); + } else { + print ''; + } + print "
'; + print $langs->trans("BoxTitleLatestModifiedMyObjects", $max); + print ''.$langs->trans("DateModificationShort").'
'.$myobjectstatic->getNomUrl(1).''; + print "'.dol_print_date($db->jdate($objp->tms), 'day')."
'.$langs->trans("None").'

"; + } +} +*/ + +print '
'; + +// End of page +llxFooter(); +$db->close(); diff --git a/img/README.md b/img/README.md new file mode 100755 index 0000000..b0e8d6b --- /dev/null +++ b/img/README.md @@ -0,0 +1,14 @@ + +Directory for module image files +-------------------------------- + +You can put here the .png files of your module: + + +If the picto of your module is an image (property $picto has been set to 'epcqr.png@epcqr', you can put into this +directory a .png file called *object_epcqr.png* (16x16 or 32x32 pixels) + + +If the picto of an object is an image (property $picto of the object.class.php has been set to 'myobject.png@epcqr', then you can put into this +directory a .png file called *object_myobject.png* (16x16 or 32x32 pixels) + diff --git a/langs/en_US/epcqr.lang b/langs/en_US/epcqr.lang new file mode 100755 index 0000000..6aa6283 --- /dev/null +++ b/langs/en_US/epcqr.lang @@ -0,0 +1,42 @@ +# Translation file + +# +# Generic +# + +# Module label 'ModuleEpcqrName' +ModuleEpcqrName = Epcqr +# Module description 'ModuleEpcqrDesc' +ModuleEpcqrDesc = Epcqr description + +# +# Admin page +# +EpcqrSetup = Epcqr setup +Settings = Settings +EpcqrSetupPage = Epcqr setup page +NewSection=New section +EPCQR_MYPARAM1 = My param 1 +EPCQR_MYPARAM1Tooltip = My param 1 tooltip +EPCQR_MYPARAM2=My param 2 +EPCQR_MYPARAM2Tooltip=My param 2 tooltip + + +# +# About page +# +About = About +EpcqrAbout = About Epcqr +EpcqrAboutPage = Epcqr about page + +# +# Sample page +# +EpcqrArea = Home Epcqr +MyPageName = My page name + +# +# Sample widget +# +MyWidget = My widget +MyWidgetDescription = My widget description diff --git a/lib/epcqr.lib.php b/lib/epcqr.lib.php new file mode 100755 index 0000000..7249620 --- /dev/null +++ b/lib/epcqr.lib.php @@ -0,0 +1,197 @@ + + * + * 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 . + */ + +/** + * \file epcqr/lib/epcqr.lib.php + * \ingroup epcqr + * \brief Library files with common functions for Epcqr + */ + +/** + * Prepare admin pages header + * + * @return array + */ +function epcqrAdminPrepareHead() +{ + global $langs, $conf; + + // global $db; + // $extrafields = new ExtraFields($db); + // $extrafields->fetch_name_optionals_label('myobject'); + + $langs->load("epcqr@epcqr"); + + $h = 0; + $head = array(); + + $head[$h][0] = dol_buildpath("/epcqr/admin/setup.php", 1); + $head[$h][1] = $langs->trans("Settings"); + $head[$h][2] = 'settings'; + $h++; + + /* + $head[$h][0] = dol_buildpath("/epcqr/admin/myobject_extrafields.php", 1); + $head[$h][1] = $langs->trans("ExtraFields"); + $nbExtrafields = (isset($extrafields->attributes['myobject']['label']) && is_countable($extrafields->attributes['myobject']['label'])) ? count($extrafields->attributes['myobject']['label']) : 0; + if ($nbExtrafields > 0) { + $head[$h][1] .= '' . $nbExtrafields . ''; + } + $head[$h][2] = 'myobject_extrafields'; + $h++; + + $head[$h][0] = dol_buildpath("/epcqr/admin/myobjectline_extrafields.php", 1); + $head[$h][1] = $langs->trans("ExtraFieldsLines"); + $nbExtrafields = (isset($extrafields->attributes['myobjectline']['label']) && is_countable($extrafields->attributes['myobjectline']['label'])) ? count($extrafields->attributes['myobject']['label']) : 0; + if ($nbExtrafields > 0) { + $head[$h][1] .= '' . $nbExtrafields . ''; + } + $head[$h][2] = 'myobject_extrafieldsline'; + $h++; + */ + + $head[$h][0] = dol_buildpath("/epcqr/admin/about.php", 1); + $head[$h][1] = $langs->trans("About"); + $head[$h][2] = 'about'; + $h++; + + // Show more tabs from modules + // Entries must be declared in modules descriptor with line + //$this->tabs = array( + // 'entity:+tabname:Title:@epcqr:/epcqr/mypage.php?id=__ID__' + //); // to add new tab + //$this->tabs = array( + // 'entity:-tabname:Title:@epcqr:/epcqr/mypage.php?id=__ID__' + //); // to remove a tab + complete_head_from_modules($conf, $langs, null, $head, $h, 'epcqr@epcqr'); + + complete_head_from_modules($conf, $langs, null, $head, $h, 'epcqr@epcqr', 'remove'); + + return $head; +} + +/** + * Generiert QR-Code für Rechnung und speichert Pfad im Extrafeld + * + * @param Facture $invoice Rechnungsobjekt + * @param DoliDB $db Database handler + * @return bool true bei Erfolg, false bei Fehler + */ +function epcqr_generateQRCodeForInvoice($invoice, $db) +{ + global $conf; + + dol_syslog("EPCQR: START generateQRCodeForInvoice für Rechnung ".$invoice->ref, LOG_DEBUG); + + // QRCodeGenerator-Klasse laden + require_once __DIR__.'/qrcode.class.php'; + + $qrGen = new QRCodeGenerator($db); + + // Bankverbindung aus Konfiguration laden + // TODO: Diese Werte sollten aus der Modul-Konfiguration kommen + $accountHolder = getDolGlobalString('EPCQR_ACCOUNT_HOLDER', 'Eduard Wisch'); + $iban = getDolGlobalString('EPCQR_IBAN', 'DE70217625500013438147'); + $bic = getDolGlobalString('EPCQR_BIC', 'GENODEF1HUM'); + + // Betrag und Referenz aus Rechnung + $amount = price2num($invoice->total_ttc, 'MT'); + $reference = $invoice->ref; + + // QR-Code generieren + $qrCodePath = $qrGen->generateEPCQRCode($accountHolder, $iban, $bic, $amount, $reference); + + if ($qrCodePath === false) { + dol_syslog("EPCQR: Fehler beim Generieren des QR-Codes", LOG_ERR); + return false; + } + + // Extrafelder aktualisieren + // qrcodepath: Pfad zur lokalen QR-Code-Datei (für ODT-Integration) + $invoice->array_options['options_qrcodepath'] = $qrCodePath; + $invoice->insertExtraFields(); + + // qrcode: HTML für Anzeige (Kompatibilität mit alter Version) + $qrCodeUrl = DOL_URL_ROOT.'/viewimage.php?modulepart=epcqr&file='.urlencode(basename(dirname($qrCodePath)).'/'.basename($qrCodePath)); + $invoice->array_options['options_qrcode'] = ""; + $invoice->insertExtraFields(); + + // qrcodepfad: URL zum QR-Code (Kompatibilität mit alter Version) + $invoice->array_options['options_qrcodepfad'] = $qrCodeUrl; + $invoice->insertExtraFields(); + + dol_syslog("EPCQR: QR-Code erfolgreich generiert: ".$qrCodePath, LOG_INFO); + + return true; +} + +/** + * Hook für ODT-Generierung: Fügt Bilder nach der Generierung ein + * + * @param string $odfFilePath Pfad zur ODT-Datei + * @param CommonObject $object Dolibarr-Objekt + * @return bool true bei Erfolg, false bei Fehler + */ +function epcqr_processODTImages($odfFilePath, $object) +{ + dol_syslog("EPCQR: START processODTImages für ".$odfFilePath, LOG_DEBUG); + + // Substitutionsfunktion laden + require_once __DIR__.'/../core/substitutions/functions_epcqr.lib.php'; + + // Bilddaten sammeln + $imageData = array(); + + // QR-Code + if (isset($object->array_options['options_qrcodepath'])) { + $qrCodePath = $object->array_options['options_qrcodepath']; + if (!empty($qrCodePath) && file_exists($qrCodePath)) { + $imageData['qrcode'] = $qrCodePath; + dol_syslog("EPCQR: QR-Code gefunden: ".$qrCodePath, LOG_DEBUG); + } + } + + // Weitere Bilder (alle Extrafelder die auf _imagepath enden) + if (!empty($object->array_options)) { + foreach ($object->array_options as $key => $value) { + if (preg_match('/^options_(.+)_imagepath$/', $key, $matches)) { + $fieldname = $matches[1]; + if (!empty($value) && file_exists($value)) { + $imageData[$fieldname] = $value; + dol_syslog("EPCQR: Bild gefunden für ".$fieldname.": ".$value, LOG_DEBUG); + } + } + } + } + + // Keine Bilder? Dann nichts zu tun + if (empty($imageData)) { + dol_syslog("EPCQR: Keine Bilder zum Einfügen gefunden", LOG_DEBUG); + return true; + } + + // Bilder in ODT einfügen + $result = epcqr_insertImagesIntoODT($odfFilePath, $imageData); + + if ($result) { + dol_syslog("EPCQR: Bilder erfolgreich in ODT eingefügt", LOG_INFO); + } else { + dol_syslog("EPCQR: Fehler beim Einfügen der Bilder in ODT", LOG_ERR); + } + + return $result; +} diff --git a/lib/qrcode.class.php b/lib/qrcode.class.php new file mode 100644 index 0000000..528d1f4 --- /dev/null +++ b/lib/qrcode.class.php @@ -0,0 +1,228 @@ + + * + * 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 . + */ + +/** + * \file lib/qrcode.class.php + * \ingroup epcqr + * \brief QR-Code Generator mit lokalem Caching + */ + +/** + * QRCode Generator Klasse + * Generiert QR-Codes und cached sie lokal für Wiederverwendung + */ +class QRCodeGenerator +{ + private $db; + private $cacheDir; + + /** + * Constructor + * + * @param DoliDB $db Database handler + */ + public function __construct($db) + { + global $conf; + + $this->db = $db; + + // Cache-Verzeichnis für QR-Codes + $this->cacheDir = $conf->epcqr->dir_output.'/qrcodes'; + + // Verzeichnis erstellen falls nicht vorhanden + if (!is_dir($this->cacheDir)) { + dol_mkdir($this->cacheDir); + } + } + + /** + * Generiert einen EPC-QR-Code für SEPA-Überweisungen + * + * @param string $accountHolder Kontoinhaber Name + * @param string $iban IBAN + * @param string $bic BIC (optional) + * @param float $amount Betrag + * @param string $reference Verwendungszweck + * @return string|false Pfad zur generierten QR-Code-Datei oder false bei Fehler + */ + public function generateEPCQRCode($accountHolder, $iban, $bic, $amount, $reference) + { + // Eindeutigen Dateinamen generieren basierend auf Parametern + $hash = md5($accountHolder.$iban.$bic.$amount.$reference); + $filename = 'epc_'.$hash.'.png'; + $filepath = $this->cacheDir.'/'.$filename; + + // Prüfen ob QR-Code bereits cached ist + if (file_exists($filepath)) { + dol_syslog("QRCodeGenerator: QR-Code aus Cache geladen: ".$filepath, LOG_DEBUG); + return $filepath; + } + + // QR-Code generieren + $qrData = $this->generateEPCData($accountHolder, $iban, $bic, $amount, $reference); + $result = $this->generateQRImage($qrData, $filepath); + + if ($result) { + dol_syslog("QRCodeGenerator: QR-Code generiert: ".$filepath, LOG_DEBUG); + return $filepath; + } + + dol_syslog("QRCodeGenerator: Fehler beim Generieren des QR-Codes", LOG_ERR); + return false; + } + + /** + * Generiert einen generischen QR-Code aus beliebigem Text + * + * @param string $data Daten für QR-Code + * @param string $prefix Präfix für Dateinamen (default: 'qr') + * @return string|false Pfad zur generierten QR-Code-Datei oder false bei Fehler + */ + public function generateQRCode($data, $prefix = 'qr') + { + // Eindeutigen Dateinamen generieren + $hash = md5($data); + $filename = $prefix.'_'.$hash.'.png'; + $filepath = $this->cacheDir.'/'.$filename; + + // Prüfen ob QR-Code bereits cached ist + if (file_exists($filepath)) { + dol_syslog("QRCodeGenerator: QR-Code aus Cache geladen: ".$filepath, LOG_DEBUG); + return $filepath; + } + + // QR-Code generieren + $result = $this->generateQRImage($data, $filepath); + + if ($result) { + dol_syslog("QRCodeGenerator: QR-Code generiert: ".$filepath, LOG_DEBUG); + return $filepath; + } + + dol_syslog("QRCodeGenerator: Fehler beim Generieren des QR-Codes", LOG_ERR); + return false; + } + + /** + * Generiert EPC-Datenstring für SEPA-QR-Codes + * + * @param string $accountHolder Kontoinhaber Name + * @param string $iban IBAN + * @param string $bic BIC + * @param float $amount Betrag + * @param string $reference Verwendungszweck + * @return string EPC-Datenstring + */ + private function generateEPCData($accountHolder, $iban, $bic, $amount, $reference) + { + // EPC QR-Code Format (GiroCode Standard) + $epcData = array( + 'BCD', // Service Tag + '002', // Version + '1', // Character set (1 = UTF-8) + 'SCT', // Identification (SEPA Credit Transfer) + $bic, // BIC + $accountHolder, // Empfänger Name + $iban, // Empfänger IBAN + 'EUR'.number_format($amount, 2, '.', ''), // Betrag + '', // Purpose (optional) + $reference, // Verwendungszweck + '' // Beneficiary to originator information (optional) + ); + + return implode("\n", $epcData); + } + + /** + * Generiert QR-Code-Bild aus Daten + * + * Nutzt zunächst den externen Service, später kann dies durch + * eine native PHP-Implementierung ersetzt werden + * + * @param string $data Daten für QR-Code + * @param string $filepath Zielpfad für PNG-Datei + * @return bool true bei Erfolg, false bei Fehler + */ + private function generateQRImage($data, $filepath) + { + // Methode 1: Externe API (aktuell) + // TODO: Später durch native PHP-Generierung ersetzen + $url = 'https://qr.data-it-solution.de/generate?data='.urlencode($data).'&size=300'; + + // Bild von URL holen + $imageData = @file_get_contents($url); + + if ($imageData === false) { + // Fallback: Versuche mit cURL + if (function_exists('curl_init')) { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + $imageData = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200 || $imageData === false) { + return false; + } + } else { + return false; + } + } + + // Bild speichern + $result = file_put_contents($filepath, $imageData); + + return ($result !== false); + } + + /** + * Löscht gecachte QR-Codes die älter als X Tage sind + * + * @param int $days Anzahl Tage (default: 30) + * @return int Anzahl gelöschter Dateien + */ + public function cleanCache($days = 30) + { + $deleted = 0; + $threshold = time() - ($days * 86400); + + if (!is_dir($this->cacheDir)) { + return 0; + } + + $files = scandir($this->cacheDir); + foreach ($files as $file) { + if ($file === '.' || $file === '..') { + continue; + } + + $filepath = $this->cacheDir.'/'.$file; + + if (is_file($filepath) && filemtime($filepath) < $threshold) { + if (unlink($filepath)) { + $deleted++; + } + } + } + + dol_syslog("QRCodeGenerator: ".$deleted." alte QR-Codes gelöscht", LOG_INFO); + return $deleted; + } +} diff --git a/lib/tbs_class.php b/lib/tbs_class.php new file mode 100755 index 0000000..06d0678 --- /dev/null +++ b/lib/tbs_class.php @@ -0,0 +1,5597 @@ +TinyButStrong Error (PHP Version Check) : Your PHP version is '.PHP_VERSION.' while TinyButStrong needs PHP version 5.0 or higher. You should try with TinyButStrong Edition for PHP 4.'; + +// Render flags +define('TBS_NOTHING', 0); +define('TBS_OUTPUT', 1); +define('TBS_EXIT', 2); + +// Plug-ins actions +define('TBS_INSTALL', -1); +define('TBS_ISINSTALLED', -3); + +// ********************************************* + +class clsTbsLocator { + + public $PosBeg = false; + public $PosEnd = false; + public $Enlarged = false; + public $FullName = false; + public $SubName = ''; + public $SubOk = false; + public $SubLst = array(); + public $SubNbr = 0; + public $PrmLst = array(); + public $PrmPos; // positions of the parameters, if asked + public $PrmIfNbr = false; + public $MagnetId = false; + public $BlockFound = false; + public $FirstMerge = true; + public $ConvProtect = true; + public $ConvStr = true; + public $ConvMode = 1; // Normal + public $ConvBr = true; + + // Compatibility with PHP 8.2 + public $Prop = array(); // dynamic properties, used by OpenTBS + + public $Ope; + public $OpeEnd; + public $PosNext; + public $PrmIf; + public $PrmThen; + public $PrmThenVar; + public $PrmIfVar; + public $PrmElseVar; + + // other + public $ConvEsc; + public $ConvWS; + public $ConvJS; + public $ConvUrl; + public $ConvUtf8; + + public $OnFrmInfo; + public $OnFrmArg; + + public $OpeUtf8; + public $OpeAct; + public $OpePrm; + public $OpeArg; + + public $OpeMOK; + public $OpeMKO; + public $MSave; + + // Sub-template + public $SaveSrc; + public $SaveMode; + public $SaveVarRef; + public $SaveRender; + + // Att + public $AttForward; + public $AttTagBeg; + public $AttTagEnd; + public $AttDelimChr; + public $AttName; + public $AttBeg; + public $AttEnd; + public $AttDelimCnt; + public $AttValBeg; + public $PosBeg0; + public $PosEnd0; + public $InsPos; + public $InsLen; + public $DelPos; + public $DelLen; + public $PosBeg2; + public $PosEnd2; + + // blocks + public $P1; + public $FieldOutside; + public $FOStop; + public $BDefLst; + public $NoData; + public $Special; + public $HeaderFound; + public $FooterFound; + public $SerialEmpty; + public $GrpBreak; + public $BoundFound; + public $CheckNext; + public $CheckPrev; + public $WhenFound; + public $WhenDefault; + public $WhenDefaultBeforeNS; + public $SectionNbr; + public $SectionLst; + public $PosDefBeg; + public $RightLevel; + public $BlockSrc; + public $PosDefEnd; + public $IsRecInfo; + public $RecInfo; + public $WhenSeveral; + public $WhenNbr; + public $WhenLst; + public $FooterNbr; + public $FooterDef; + public $HeaderNbr; + public $HeaderDef; + public $ValPrev; + public $BoundLst; + public $BoundNb; + public $BoundSingleNb; + public $ValNext; + +} + +// ********************************************* + +class clsTbsDataSource { + +public $Type = false; +public $SubType = 0; +public $SrcId = false; +public $Query = ''; +public $RecSet = false; +public $RecNumInit = 0; // Used by ByPage plugin +public $RecSaving = false; +public $RecSaved = false; +public $RecBuffer = false; +public $TBS = false; +public $OnDataOk = false; +public $OnDataPrm = false; +public $OnDataPrmDone = array(); +public $OnDataPi = false; + +// Info relative to the current record : +public $CurrRec = false; // Used by ByPage plugin +public $RecKey = ''; // Used by ByPage plugin +public $RecNum = 0; // Used by ByPage plugin + +public $PrevRec = null; +public $NextRec = null; + +public $PrevSave = false; +public $NextSave = false; + +// Compatibility with PHP 8.2 +public $Prop = array(); // Used by ByPage plugin +public $RecNbr; +public $RSIsFirst; +public $NumMin; +public $NumMax; +public $NumStep; +public $NumVal; +public $OnDataPrmRef; +public $OnDataArgs; + +public function DataAlert($Msg) { + if (is_array($this->TBS->_CurrBlock)) { + return $this->TBS->meth_Misc_Alert('when merging block "'.implode(',',$this->TBS->_CurrBlock).'"',$Msg); + } else { + return $this->TBS->meth_Misc_Alert('when merging block '.$this->TBS->_ChrOpen.$this->TBS->_CurrBlock.$this->TBS->_ChrClose,$Msg); + } +} + +public function DataPrepare(&$SrcId,&$TBS) { + + $this->SrcId = &$SrcId; + $this->TBS = &$TBS; + $FctInfo = false; + $FctObj = false; + + if (is_array($SrcId)) { + $this->Type = 0; + } elseif (is_resource($SrcId)) { + + $Key = get_resource_type($SrcId); + switch ($Key) { + case 'mysql link' : $this->Type = 6; break; + case 'mysql link persistent' : $this->Type = 6; break; + case 'mysql result' : $this->Type = 6; $this->SubType = 1; break; + case 'pgsql link' : $this->Type = 7; break; + case 'pgsql link persistent' : $this->Type = 7; break; + case 'pgsql result' : $this->Type = 7; $this->SubType = 1; break; + case 'sqlite database' : $this->Type = 8; break; + case 'sqlite database (persistent)' : $this->Type = 8; break; + case 'sqlite result' : $this->Type = 8; $this->SubType = 1; break; + default : + $FctInfo = $Key; + $FctCat = 'r'; + } + + } elseif (is_string($SrcId)) { + + switch (strtolower($SrcId)) { + case 'array' : $this->Type = 0; $this->SubType = 1; break; + case 'clear' : $this->Type = 0; $this->SubType = 3; break; + case 'mysql' : $this->Type = 6; $this->SubType = 2; break; + case 'text' : $this->Type = 2; break; + case 'num' : $this->Type = 1; break; + default : + $FctInfo = $SrcId; + $FctCat = 'k'; + } + + } elseif ($SrcId instanceof Iterator) { + $this->Type = 9; $this->SubType = 1; + } elseif ($SrcId instanceof ArrayObject) { + $this->Type = 9; $this->SubType = 2; + } elseif ($SrcId instanceof IteratorAggregate) { + $this->Type = 9; $this->SubType = 3; + } elseif ($SrcId instanceof MySQLi) { + $this->Type = 10; + } elseif ($SrcId instanceof PDO) { + $this->Type = 11; + } elseif ($SrcId instanceof Zend_Db_Adapter_Abstract) { + $this->Type = 12; + } elseif ($SrcId instanceof SQLite3) { + $this->Type = 13; $this->SubType = 1; + } elseif ($SrcId instanceof SQLite3Stmt) { + $this->Type = 13; $this->SubType = 2; + } elseif ($SrcId instanceof SQLite3Result) { + $this->Type = 13; $this->SubType = 3; + } elseif (is_a($SrcId, 'Doctrine\DBAL\Connection')) { + $this->Type = 14; + } elseif (is_object($SrcId)) { + $FctInfo = get_class($SrcId); + $FctCat = 'o'; + $FctObj = &$SrcId; + $this->SrcId = &$SrcId; + } elseif ($SrcId===false) { + $this->DataAlert('the specified source is set to FALSE. Maybe your connection has failed.'); + } else { + $this->DataAlert('unsupported variable type : \''.gettype($SrcId).'\'.'); + } + + if ($FctInfo!==false) { + $ErrMsg = false; + if ($TBS->meth_Misc_UserFctCheck($FctInfo,$FctCat,$FctObj,$ErrMsg,false)) { + $this->Type = $FctInfo['type']; + if ($this->Type!==5) { + if ($this->Type===4) { + $this->FctPrm = array(false,0); + $this->SrcId = &$FctInfo['open'][0]; + } + $this->FctOpen = &$FctInfo['open']; + $this->FctFetch = &$FctInfo['fetch']; + $this->FctClose = &$FctInfo['close']; + } + } else { + $this->Type = $this->DataAlert($ErrMsg); + } + } + + return ($this->Type!==false); + +} + +public function DataOpen(&$Query,$QryPrms=false) { + + // Init values + unset($this->CurrRec); + $this->CurrRec = true; + + if ($this->RecSaved) { + $this->RSIsFirst = true; + unset($this->RecKey); $this->RecKey = ''; + $this->RecNum = $this->RecNumInit; + if ($this->OnDataOk) $this->OnDataArgs[1] = &$this->CurrRec; + return true; + } + + unset($this->RecSet); + $this->RecSet = false; + $this->RecNumInit = 0; + $this->RecNum = 0; + + // Previous and next records + $this->PrevRec = (object) null; + $this->NextRec = false; + + if (isset($this->TBS->_piOnData)) { + $this->OnDataPi = true; + $this->OnDataPiRef = &$this->TBS->_piOnData; + $this->OnDataOk = true; + } + if ($this->OnDataOk) { + $this->OnDataArgs = array(); + $this->OnDataArgs[0] = &$this->TBS->_CurrBlock; + $this->OnDataArgs[1] = &$this->CurrRec; + $this->OnDataArgs[2] = &$this->RecNum; + $this->OnDataArgs[3] = &$this->TBS; + } + + switch ($this->Type) { + case 0: // Array + if (($this->SubType===1) && (is_string($Query))) $this->SubType = 2; + if ($this->SubType===0) { + $this->RecSet = &$this->SrcId; + } elseif ($this->SubType===1) { + if (is_array($Query)) { + $this->RecSet = &$Query; + } else { + $this->DataAlert('type \''.gettype($Query).'\' not supported for the Query Parameter going with \'array\' Source Type.'); + } + } elseif ($this->SubType===2) { + // TBS query string for array and objects, syntax: "var[item1][item2]->item3[item4]..." + $x = trim($Query); + $z = chr(0); + $x = str_replace(array(']->','][','->','['),$z,$x); + if (substr($x,strlen($x)-1,1)===']') $x = substr($x,0,strlen($x)-1); + $ItemLst = explode($z,$x); + $ItemNbr = count($ItemLst); + $Item0 = &$ItemLst[0]; + // Check first item + if ($Item0[0]==='~') { + $Item0 = substr($Item0,1); + if ($this->TBS->ObjectRef!==false) { + $Var = &$this->TBS->ObjectRef; + $i = 0; + } else { + $i = $this->DataAlert('invalid query \''.$Query.'\' because property ObjectRef is not set.'); + } + } else { + if ( is_null($this->TBS->VarRef) && isset($GLOBALS[$Item0]) ) { + $Var = &$GLOBALS[$Item0]; + $i = 1; + } elseif (isset($this->TBS->VarRef[$Item0])) { + $Var = &$this->TBS->VarRef[$Item0]; + $i = 1; + } else { + $i = $this->DataAlert('invalid query \''.$Query.'\' because VarRef item \''.$Item0.'\' is not found.'); + } + } + // Check sub-items + $Empty = false; + while (($i!==false) && ($i<$ItemNbr) && ($Empty===false)) { + $x = $ItemLst[$i]; + if (is_array($Var)) { + if (isset($Var[$x])) { + $Var = &$Var[$x]; + } else { + $Empty = true; + } + } elseif (is_object($Var)) { + $form = $this->TBS->f_Misc_ParseFctForm($x); + $n = $form['name']; + if ( method_exists($Var, $n) || ($form['as_fct'] && method_exists($Var,'__call')) ) { + $f = array(&$Var,$n); unset($Var); + $Var = call_user_func_array($f,$form['args']); + } elseif (property_exists(get_class($Var),$n)) { + if (isset($Var->$n)) $Var = &$Var->$n; + } elseif (isset($Var->$n)) { + $Var = $Var->$n; // useful for overloaded property + } else { + $Empty = true; + } + } else { + $i = $this->DataAlert('invalid query \''.$Query.'\' because item \''.$ItemLst[$i].'\' is neither an Array nor an Object. Its type is \''.gettype($Var).'\'.'); + } + if ($i!==false) $i++; + } + // Assign data + if ($i!==false) { + if ($Empty) { + $this->RecSet = array(); + } else { + $this->RecSet = &$Var; + } + } + } elseif ($this->SubType===3) { // Clear + $this->RecSet = array(); + } + // First record + if ($this->RecSet!==false) { + $this->RecNbr = $this->RecNumInit + count($this->RecSet); + $this->RSIsFirst = true; + $this->RecSaved = true; + $this->RecSaving = false; + } + break; + case 6: // MySQL + switch ($this->SubType) { + case 0: $this->RecSet = @mysql_query($Query,$this->SrcId); break; + case 1: $this->RecSet = $this->SrcId; break; + case 2: $this->RecSet = @mysql_query($Query); break; + } + if ($this->RecSet===false) $this->DataAlert('MySql error message when opening the query: '.mysql_error()); + break; + case 1: // Num + $this->RecSet = true; + $this->NumMin = 1; + $this->NumMax = 1; + $this->NumStep = 1; + if (is_array($Query)) { + if (isset($Query['min'])) $this->NumMin = $Query['min']; + if (isset($Query['step'])) $this->NumStep = $Query['step']; + if (isset($Query['max'])) { + $this->NumMax = $Query['max']; + } else { + $this->RecSet = $this->DataAlert('the \'num\' source is an array that has no value for the \'max\' key.'); + } + if ($this->NumStep==0) $this->RecSet = $this->DataAlert('the \'num\' source is an array that has a step value set to zero.'); + } else { + $this->NumMax = ceil($Query); + } + if ($this->RecSet) { + if ($this->NumStep>0) { + $this->NumVal = $this->NumMin; + } else { + $this->NumVal = $this->NumMax; + } + } + break; + case 2: // Text + if (is_string($Query)) { + $this->RecSet = &$Query; + } else { + $this->RecSet = $this->TBS->meth_Misc_ToStr($Query); + } + break; + case 3: // Custom function + $FctOpen = $this->FctOpen; + $this->RecSet = $FctOpen($this->SrcId,$Query,$QryPrms); + if ($this->RecSet===false) $this->DataAlert('function '.$FctOpen.'() has failed to open query {'.$Query.'}'); + break; + case 4: // Custom method from ObjectRef + $this->RecSet = call_user_func_array($this->FctOpen,array(&$this->SrcId,&$Query,&$QryPrms)); + if ($this->RecSet===false) $this->DataAlert('method '.get_class($this->FctOpen[0]).'::'.$this->FctOpen[1].'() has failed to open query {'.$Query.'}'); + break; + case 5: // Custom method of object + $this->RecSet = $this->SrcId->tbsdb_open($this->SrcId,$Query,$QryPrms); + if ($this->RecSet===false) $this->DataAlert('method '.get_class($this->SrcId).'::tbsdb_open() has failed to open query {'.$Query.'}'); + break; + case 7: // PostgreSQL + switch ($this->SubType) { + case 0: $this->RecSet = @pg_query($this->SrcId,$Query); break; + case 1: $this->RecSet = $this->SrcId; break; + } + if ($this->RecSet===false) $this->DataAlert('PostgreSQL error message when opening the query: '.pg_last_error($this->SrcId)); + break; + case 8: // SQLite + switch ($this->SubType) { + case 0: $this->RecSet = @sqlite_query($this->SrcId,$Query); break; + case 1: $this->RecSet = $this->SrcId; break; + } + if ($this->RecSet===false) $this->DataAlert('SQLite error message when opening the query:'.sqlite_error_string(sqlite_last_error($this->SrcId))); + break; + case 9: // Iterator + if ($this->SubType==1) { + $this->RecSet = $this->SrcId; + } else { // 2 or 3 + $this->RecSet = $this->SrcId->getIterator(); + } + $this->RecSet->rewind(); + break; + case 10: // MySQLi + $this->RecSet = $this->SrcId->query($Query); + if ($this->RecSet===false) $this->DataAlert('MySQLi error message when opening the query:'.$this->SrcId->error); + break; + case 11: // PDO + $this->RecSet = $this->SrcId->prepare($Query); + if ($this->RecSet===false) { + $ok = false; + } else { + if (!is_array($QryPrms)) $QryPrms = array(); + $ok = $this->RecSet->execute($QryPrms); + } + if (!$ok) { + $err = $this->SrcId->errorInfo(); + $this->DataAlert('PDO error message when opening the query:'.$err[2]); + } + break; + case 12: // Zend_DB_Adapter + try { + if (!is_array($QryPrms)) $QryPrms = array(); + $this->RecSet = $this->SrcId->query($Query, $QryPrms); + } catch (Exception $e) { + $this->DataAlert('Zend_DB_Adapter error message when opening the query: '.$e->getMessage()); + } + break; + case 13: // SQLite3 + try { + if ($this->SubType==3) { + $this->RecSet = $this->SrcId; + } elseif (($this->SubType==1) && (!is_array($QryPrms))) { + // SQL statement without parameters + $this->RecSet = $this->SrcId->query($Query); + } else { + if ($this->SubType==2) { + $stmt = $this->SrcId; + $prms = $Query; + } else { + // SQL statement with parameters + $stmt = $this->SrcId->prepare($Query); + $prms = $QryPrms; + } + // bind parameters + if (is_array($prms)) { + foreach ($prms as $p => $v) { + if (is_numeric($p)) { + $p = $p + 1; + } + if (is_array($v)) { + $stmt->bindValue($p, $v[0], $v[1]); + } else { + $stmt->bindValue($p, $v); + } + } + } + $this->RecSet = $stmt->execute(); + } + } catch (Exception $e) { + $this->DataAlert('SQLite3 error message when opening the query: '.$e->getMessage()); + } + break; + case 14: // Doctrine DBAL + try { + if (!is_array($QryPrms)) $QryPrms = array(); + $this->RecSet = $this->SrcId->executeQuery($Query, $QryPrms); + } catch (Exception $e) { + $this->DataAlert('Doctrine DBAL error message when opening the query: '.$e->getMessage()); + } + break; + } + + if (($this->Type===0) || ($this->Type===9)) { + unset($this->RecKey); $this->RecKey = ''; + } else { + if ($this->RecSaving) { + unset($this->RecBuffer); $this->RecBuffer = array(); + } + $this->RecKey = &$this->RecNum; // Not array: RecKey = RecNum + } + + return ($this->RecSet!==false); + +} + +public function DataFetch() { + + // Save previous record + if ($this->PrevSave) { + $this->_CopyRec($this, $this->PrevRec); + } + + if ($this->NextSave) { + // set current record + if ($this->NextRec === false) { + // first record + $this->NextRec = (object) array('RecNum' => 1); // prepare for getting properties, RecNum needed for the first fetch + $this->_DataFetchOn($this); + } else { + // other records + $this->_CopyRec($this->NextRec, $this); + } + // set next record + if ($this->CurrRec === false) { + // no more record + $this->NextRec = (object) null; // clear properties + } else { + $this->_DataFetchOn($this->NextRec); + } + } else { + // Classic fetch + $this->_DataFetchOn($this); + } + +} + +public function DataClose() { + $this->OnDataOk = false; + $this->OnDataPrm = false; + $this->OnDataPi = false; + if ($this->RecSaved) return; + switch ($this->Type) { + case 6: mysql_free_result($this->RecSet); break; + case 3: $FctClose=$this->FctClose; $FctClose($this->RecSet); break; + case 4: call_user_func_array($this->FctClose,array(&$this->RecSet)); break; + case 5: $this->SrcId->tbsdb_close($this->RecSet); break; + case 7: pg_free_result($this->RecSet); break; + case 10: $this->RecSet->free(); break; // MySQLi + case 13: // SQLite3 + if ($this->SubType!=3) { + $this->RecSet->finalize(); + } + break; + //case 11: $this->RecSet->closeCursor(); break; // PDO + } + if ($this->RecSaving) { + $this->RecSet = &$this->RecBuffer; + $this->RecNbr = $this->RecNumInit + count($this->RecSet); + $this->RecSaving = false; + $this->RecSaved = true; + } +} + +/** + * Copy the record information from an object to another. + */ +private function _CopyRec($from, $to) { + + $to->CurrRec = $from->CurrRec; + $to->RecNum = $from->RecNum; + $to->RecKey = $from->RecKey; + +} + +/** + * Fetch the next record on the object $obj. + * This wil set the proiperties : + * $obj->CurrRec + * $obj->RecKey + * $obj->RecNum + */ +private function _DataFetchOn($obj) { + + // Check if the records are saved in an array + if ($this->RecSaved) { + if ($obj->RecNum < $this->RecNbr) { + if ($this->RSIsFirst) { + if ($this->SubType===2) { // From string + reset($this->RecSet); + $obj->RecKey = key($this->RecSet); + $obj->CurrRec = &$this->RecSet[$obj->RecKey]; + } else { + $obj->CurrRec = reset($this->RecSet); + $obj->RecKey = key($this->RecSet); + } + $this->RSIsFirst = false; + } else { + if ($this->SubType===2) { // From string + next($this->RecSet); + $obj->RecKey = key($this->RecSet); + $obj->CurrRec = &$this->RecSet[$obj->RecKey]; + } else { + $obj->CurrRec = next($this->RecSet); + $obj->RecKey = key($this->RecSet); + } + } + if ((!is_array($obj->CurrRec)) && (!is_object($obj->CurrRec))) $obj->CurrRec = array('key'=>$obj->RecKey, 'val'=>$obj->CurrRec); + $obj->RecNum++; + if ($this->OnDataOk) { + $this->OnDataArgs[1] = &$obj->CurrRec; // Reference has changed if ($this->SubType===2) + if ($this->OnDataPrm) call_user_func_array($this->OnDataPrmRef,$this->OnDataArgs); + if ($this->OnDataPi) $this->TBS->meth_PlugIn_RunAll($this->OnDataPiRef,$this->OnDataArgs); + if ($this->SubType!==2) $this->RecSet[$obj->RecKey] = $obj->CurrRec; // save modifications because array reading is done without reference :( + } + } else { + unset($obj->CurrRec); $obj->CurrRec = false; + } + return; + } + + switch ($this->Type) { + case 6: // MySQL + $obj->CurrRec = mysql_fetch_assoc($this->RecSet); + break; + case 1: // Num + if (($this->NumVal>=$this->NumMin) && ($this->NumVal<=$this->NumMax)) { + $obj->CurrRec = array('val'=>$this->NumVal); + $this->NumVal += $this->NumStep; + } else { + $obj->CurrRec = false; + } + break; + case 2: // Text + if ($obj->RecNum===0) { + if ($this->RecSet==='') { + $obj->CurrRec = false; + } else { + $obj->CurrRec = &$this->RecSet; + } + } else { + $obj->CurrRec = false; + } + break; + case 3: // Custom function + $FctFetch = $this->FctFetch; + $obj->CurrRec = $FctFetch($this->RecSet,$obj->RecNum+1); + break; + case 4: // Custom method from ObjectRef + $this->FctPrm[0] = &$this->RecSet; $this->FctPrm[1] = $obj->RecNum+1; + $obj->CurrRec = call_user_func_array($this->FctFetch,$this->FctPrm); + break; + case 5: // Custom method of object + $obj->CurrRec = $this->SrcId->tbsdb_fetch($this->RecSet,$obj->RecNum+1); + break; + case 7: // PostgreSQL + $obj->CurrRec = pg_fetch_assoc($this->RecSet); + break; + case 8: // SQLite + $obj->CurrRec = sqlite_fetch_array($this->RecSet,SQLITE_ASSOC); + break; + case 9: // Iterator + if ($this->RecSet->valid()) { + $obj->CurrRec = $this->RecSet->current(); + $obj->RecKey = $this->RecSet->key(); + $this->RecSet->next(); + } else { + $obj->CurrRec = false; + } + break; + case 10: // MySQLi + $obj->CurrRec = $this->RecSet->fetch_assoc(); + if (is_null($obj->CurrRec)) $obj->CurrRec = false; + break; + case 11: // PDO + $obj->CurrRec = $this->RecSet->fetch(PDO::FETCH_ASSOC); + break; + case 12: // Zend_DB_Adapater + $obj->CurrRec = $this->RecSet->fetch(Zend_Db::FETCH_ASSOC); + break; + case 13: // SQLite3 + $obj->CurrRec = $this->RecSet->fetchArray(SQLITE3_ASSOC); + break; + case 14: // Doctrine DBAL + $obj->CurrRec = $this->RecSet->fetchAssociative(); + break; + } + + // Set the row count + if ($obj->CurrRec!==false) { + $obj->RecNum++; + if ($this->OnDataOk) { + if ($this->OnDataPrm) call_user_func_array($this->OnDataPrmRef,$this->OnDataArgs); + if ($this->OnDataPi) $this->TBS->meth_PlugIn_RunAll($this->OnDataPiRef,$this->OnDataArgs); + } + if ($this->RecSaving) $this->RecBuffer[$obj->RecKey] = $obj->CurrRec; + } + +} + +} + +// ********************************************* + +class clsTinyButStrong { + +// Public properties +public $Source = ''; +public $Render = 3; +public $TplVars = array(); +public $ObjectRef = false; +public $NoErr = false; +public $Assigned = array(); +public $ExtendedMethods = array(); +public $ErrCount = 0; +// Undocumented (can change at any version) +public $Version = '3.15.0'; +public $Charset = ''; +public $TurboBlock = true; +public $VarPrefix = ''; +public $VarRef = null; +public $FctPrefix = ''; +public $Protect = true; +public $ErrMsg = ''; +public $AttDelim = false; +public $MethodsAllowed = false; +public $ScriptsAllowed = false; +public $OnLoad = true; +public $OnShow = true; +public $IncludePath = array(); +public $TplStore = array(); +public $OldSubTpl = false; // turn to true to have compatibility with the old way to perform subtemplates, that is get output buffuring +// Private +public $_ErrMsgName = ''; +public $_LastFile = ''; +public $_CharsetFct = false; +public $_Mode = 0; +public $_CurrBlock = ''; +public $_ChrOpen = '['; +public $_ChrClose = ']'; +public $_ChrVal = '[val]'; +public $_ChrProtect = '['; +public $_PlugIns = array(); +public $_PlugIns_Ok = false; +public $_piOnFrm_Ok = false; + +// Compatibility with PHP 8.2 +private $_UserFctLst; +private $_Subscript; +public $CurrPrm; + +private $_piOnData; +private $_piBeforeLoadTemplate; +private $_piAfterLoadTemplate; +private $_piOnMergeField; +private $_piBeforeShow; +private $_piAfterShow; +private $_piOnCommand; +private $_piOnOperation; +private $_piOnCacheField; +private $_PlugIns_Ok_save; +private $_piOnFrm_Ok_save; +private $_piOnFormat; +private $_piBeforeMergeBlock; +private $_piOnMergeSection; +private $_piOnMergeGroup; +private $_piAfterMergeBlock; +private $_piOnSpecialVar; + +// OpenTBS +public $OtbsAutoLoad; +public $OtbsConvBr; +public $OtbsAutoUncompress; +public $OtbsConvertApostrophes; +public $OtbsSpacePreserve; +public $OtbsClearWriter; +public $OtbsClearMsWord; +public $OtbsMsExcelConsistent; +public $OtbsMsExcelExplicitRef; +public $OtbsClearMsPowerpoint; +public $OtbsGarbageCollector; +public $OtbsMsExcelCompatibility; +public $OtbsCurrFile; +public $OtbsSubFileLst; +public $TbsZip; + +function __construct($Options=null,$VarPrefix='',$FctPrefix='') { + + // Compatibility + if (is_string($Options)) { + $Chrs = $Options; + $Options = array('var_prefix'=>$VarPrefix, 'fct_prefix'=>$FctPrefix); + if ($Chrs!=='') { + $Err = true; + $Len = strlen($Chrs); + if ($Len===2) { // For compatibility + $Options['chr_open'] = $Chrs[0]; + $Options['chr_close'] = $Chrs[1]; + $Err = false; + } else { + $Pos = strpos($Chrs,','); + if (($Pos!==false) && ($Pos>0) && ($Pos<$Len-1)) { + $Options['chr_open'] = substr($Chrs,0,$Pos); + $Options['chr_close'] = substr($Chrs,$Pos+1); + $Err = false; + } + } + if ($Err) $this->meth_Misc_Alert('with clsTinyButStrong() function','value \''.$Chrs.'\' is a bad tag delimitor definition.'); + } + } + + // Set VarRef initial value + $this->ResetVarRef(true); + + // Set options + if (is_array($Options)) $this->SetOption($Options); + + // Links to global variables (cannot be converted to static yet because of compatibility) + global $_TBS_FormatLst, $_TBS_UserFctLst, $_TBS_BlockAlias, $_TBS_PrmCombo, $_TBS_AutoInstallPlugIns, $_TBS_ParallelLst; + if (!isset($_TBS_FormatLst)) $_TBS_FormatLst = array(); + if (!isset($_TBS_UserFctLst)) $_TBS_UserFctLst = array(); + if (!isset($_TBS_BlockAlias)) $_TBS_BlockAlias = array(); + if (!isset($_TBS_PrmCombo)) $_TBS_PrmCombo = array(); + if (!isset($_TBS_ParallelLst)) $_TBS_ParallelLst = array(); + $this->_UserFctLst = &$_TBS_UserFctLst; + + // Auto-installing plug-ins + if (isset($_TBS_AutoInstallPlugIns)) foreach ($_TBS_AutoInstallPlugIns as $pi) $this->PlugIn(TBS_INSTALL,$pi); + +} + +function __call($meth, $args) { + if (isset($this->ExtendedMethods[$meth])) { + if ( is_array($this->ExtendedMethods[$meth]) || is_string($this->ExtendedMethods[$meth]) ) { + return call_user_func_array($this->ExtendedMethods[$meth], $args); + } else { + return call_user_func_array(array(&$this->ExtendedMethods[$meth], $meth), $args); + } + } else { + $this->meth_Misc_Alert('Method not found','\''.$meth.'\' is neither a native nor an extended method of TinyButStrong.'); + } +} + +function SetOption($o, $v=false, $d=false) { + if (!is_array($o)) $o = array($o=>$v); + if (isset($o['var_prefix'])) $this->VarPrefix = $o['var_prefix']; + if (isset($o['fct_prefix'])) $this->FctPrefix = $o['fct_prefix']; + if (isset($o['noerr'])) $this->NoErr = $o['noerr']; + if (isset($o['old_subtemplate'])) $this->OldSubTpl = $o['old_subtemplate']; + if (isset($o['auto_merge'])) { + $this->OnLoad = $o['auto_merge']; + $this->OnShow = $o['auto_merge']; + } + if (isset($o['onload'])) $this->OnLoad = $o['onload']; + if (isset($o['onshow'])) $this->OnShow = $o['onshow']; + if (isset($o['att_delim'])) $this->AttDelim = $o['att_delim']; + if (isset($o['protect'])) $this->Protect = $o['protect']; + if (isset($o['turbo_block'])) $this->TurboBlock = $o['turbo_block']; + if (isset($o['charset'])) $this->meth_Misc_Charset($o['charset']); + + $UpdateChr = false; + if (isset($o['chr_open'])) { + $this->_ChrOpen = $o['chr_open']; + $UpdateChr = true; + } + if (isset($o['chr_close'])) { + $this->_ChrClose = $o['chr_close']; + $UpdateChr = true; + } + if ($UpdateChr) { + $this->_ChrVal = $this->_ChrOpen.'val'.$this->_ChrClose; + $this->_ChrProtect = '&#'.ord($this->_ChrOpen[0]).';'.substr($this->_ChrOpen,1); + } + if (array_key_exists('tpl_frms',$o)) self::f_Misc_UpdateArray($GLOBALS['_TBS_FormatLst'], 'frm', $o['tpl_frms'], $d); + if (array_key_exists('block_alias',$o)) self::f_Misc_UpdateArray($GLOBALS['_TBS_BlockAlias'], false, $o['block_alias'], $d); + if (array_key_exists('prm_combo',$o)) self::f_Misc_UpdateArray($GLOBALS['_TBS_PrmCombo'], 'prm', $o['prm_combo'], $d); + if (array_key_exists('parallel_conf',$o)) self::f_Misc_UpdateArray($GLOBALS['_TBS_ParallelLst'], false, $o['parallel_conf'], $d); + if (array_key_exists('include_path',$o)) self::f_Misc_UpdateArray($this->IncludePath, true, $o['include_path'], $d); + if (isset($o['render'])) $this->Render = $o['render']; + if (isset($o['methods_allowed'])) $this->MethodsAllowed = $o['methods_allowed']; + if (isset($o['scripts_allowed'])) $this->ScriptsAllowed = $o['scripts_allowed']; +} + +function GetOption($o) { + if ($o==='all') { + $x = explode(',', 'var_prefix,fct_prefix,noerr,auto_merge,onload,onshow,att_delim,protect,turbo_block,charset,chr_open,chr_close,tpl_frms,block_alias,parallel_conf,include_path,render,prm_combo'); + $r = array(); + foreach ($x as $o) $r[$o] = $this->GetOption($o); + return $r; + } + if ($o==='var_prefix') return $this->VarPrefix; + if ($o==='fct_prefix') return $this->FctPrefix; + if ($o==='noerr') return $this->NoErr; + if ($o==='auto_merge') return ($this->OnLoad && $this->OnShow); + if ($o==='onload') return $this->OnLoad; + if ($o==='onshow') return $this->OnShow; + if ($o==='att_delim') return $this->AttDelim; + if ($o==='protect') return $this->Protect; + if ($o==='turbo_block') return $this->TurboBlock; + if ($o==='charset') return $this->Charset; + if ($o==='chr_open') return $this->_ChrOpen; + if ($o==='chr_close') return $this->_ChrClose; + if ($o==='tpl_frms') { + // simplify the list of formats + $x = array(); + foreach ($GLOBALS['_TBS_FormatLst'] as $s=>$i) $x[$s] = $i['Str']; + return $x; + } + if ($o==='include_path') return $this->IncludePath; + if ($o==='render') return $this->Render; + if ($o==='methods_allowed') return $this->MethodsAllowed; + if ($o==='scripts_allowed') return $this->ScriptsAllowed; + if ($o==='parallel_conf') return $GLOBALS['_TBS_ParallelLst']; + if ($o==='block_alias') return $GLOBALS['_TBS_BlockAlias']; + if ($o==='prm_combo') return $GLOBALS['_TBS_PrmCombo']; + return $this->meth_Misc_Alert('with GetOption() method','option \''.$o.'\' is not supported.');; +} + +public function ResetVarRef($ToGlobal) { + // We set a new variable in order to force the reference + // value NULL means that VarRef refers to $GLOBALS + $x = ($ToGlobal) ? null : array(); + $this->VarRef = &$x; +} + +/** + * Get an item value from VarRef. + * Ensure the compatibility with PHP 8.1 if VarRef is set to Global. + * + * @param string $key The item key. + * @param mixed $default The default value. + * + * @return mixed + */ +public function GetVarRefItem($key, $default) { + + if (is_null($this->VarRef)) { + + if (array_key_exists($key, $GLOBALS)) { + return $GLOBALS[$key]; + } else { + return $default; + } + + } else { + + if (array_key_exists($key, $this->VarRef)) { + return $this->VarRef[$key]; + } else { + return $default; + } + + } + +} + +/** + * Set an item value to VarRef. + * Ensure the compatibility with PHP 8.1 if VarRef is set to Global. + * + * @param string|array $keyOrList A list of keys and items to add, or the item key. + * @param mixed $value (optional) The item value. Use NULL in order to delete the item. + */ +public function SetVarRefItem($keyOrList, $value = null) { + + if (is_array($keyOrList)) { + $list = $keyOrList; + } else { + $list = array($keyOrList => $value); + } + + if (is_null($this->VarRef)) { + + foreach ($list as $key => $value) { + if (is_null($value)) { + unset($GLOBALS[$key]); + } else { + $GLOBALS[$key] = $value; + } + } + + } else { + + foreach ($list as $key => $value) { + if (is_null($value)) { + unset($this->VarRef[$key]); + } else { + $this->VarRef[$key] = $value; + } + } + + } + +} + +// Public methods +public function LoadTemplate($File,$Charset='') { + if ($File==='') { + $this->meth_Misc_Charset($Charset); + return true; + } + $Ok = true; + if ($this->_PlugIns_Ok) { + if (isset($this->_piBeforeLoadTemplate) || isset($this->_piAfterLoadTemplate)) { + // Plug-ins + $ArgLst = func_get_args(); + $ArgLst[0] = &$File; + $ArgLst[1] = &$Charset; + if (isset($this->_piBeforeLoadTemplate)) $Ok = $this->meth_PlugIn_RunAll($this->_piBeforeLoadTemplate,$ArgLst); + } + } + // Load the file + if ($Ok!==false) { + if (!is_null($File)) { + $x = ''; + if (!$this->f_Misc_GetFile($x, $File, $this->_LastFile, $this->IncludePath)) return $this->meth_Misc_Alert('with LoadTemplate() method','file \''.$File.'\' is not found or not readable.'); + if ($Charset==='+') { + $this->Source .= $x; + } else { + $this->Source = $x; + } + } + if ($this->meth_Misc_IsMainTpl()) { + if (!is_null($File)) $this->_LastFile = $File; + if ($Charset!=='+') $this->TplVars = array(); + $this->meth_Misc_Charset($Charset); + } + // Automatic fields and blocks + if ($this->OnLoad) $this->meth_Merge_AutoOn($this->Source,'onload',true,true); + } + // Plug-ins + if ($this->_PlugIns_Ok && isset($ArgLst) && isset($this->_piAfterLoadTemplate)) $Ok = $this->meth_PlugIn_RunAll($this->_piAfterLoadTemplate,$ArgLst); + return $Ok; +} + +public function GetBlockSource($BlockName,$AsArray=false,$DefTags=true,$ReplaceWith=false) { + $RetVal = array(); + $Nbr = 0; + $Pos = 0; + $FieldOutside = false; + $P1 = false; + $Mode = ($DefTags) ? 3 : 2; + $PosBeg1 = 0; + while ($Loc = $this->meth_Locator_FindBlockNext($this->Source,$BlockName,$Pos,'.',$Mode,$P1,$FieldOutside)) { + $Nbr++; + $Sep = ''; + if ($Nbr==1) { + $PosBeg1 = $Loc->PosBeg; + } elseif (!$AsArray) { + $Sep = substr($this->Source,$PosSep,$Loc->PosBeg-$PosSep); // part of the source between sections + } + $RetVal[$Nbr] = $Sep.$Loc->BlockSrc; + $Pos = $Loc->PosEnd; + $PosSep = $Loc->PosEnd+1; + $P1 = false; + } + if ($Nbr==0) return false; + if (!$AsArray) { + if ($DefTags) { + // Return the true part of the template + $RetVal = substr($this->Source,$PosBeg1,$Pos-$PosBeg1+1); + } else { + // Return the concatenated section without def tags + $RetVal = implode('', $RetVal); + } + } + if ($ReplaceWith!==false) $this->Source = substr($this->Source,0,$PosBeg1).$ReplaceWith.substr($this->Source,$Pos+1); + return $RetVal; +} + +/** + * Get the value of a XML-HTML attribute targeted thanks to a TBS fields having parameter att. + * @param string $Name Name of the TBS fields. It must have parameter att. + * @param boolean $delete (optional, true by default) Use true to delete the TBS field. + * @return string|true|null|false The value of the attribute, + * true if the attribute is found without value, + * null if the TBS field, the target element is not found, + * or false for other error. + */ +public function GetAttValue($Name, $delete = true) { + $Pos = 0; + $val = null; + while ($Loc = $this->meth_Locator_FindTbs($this->Source,$Name,$Pos,'.')) { + if (isset($Loc->PrmLst['att'])) { + if ($this->f_Xml_AttFind($this->Source,$Loc,false,$this->AttDelim)) { + $val = false; + if ($Loc->AttBeg !== false) { + if ($Loc->AttValBeg !== false) { + $val = substr($this->Source, $Loc->AttValBeg, $Loc->AttEnd - $Loc->AttValBeg + 1); + $val = substr($val, 1, -1); + } else { + $val = true; + } + } else { + // not found + } + } else { + // att not found + } + } else { + // no att parameter + } + + if ($delete) { + $this->Source = substr_replace($this->Source, '', $Loc->PosBeg, $Loc->PosEnd - $Loc->PosBeg + 1); + $Pos = $Loc->PosBeg; + } else { + $Pos = $Loc->PosEnd; + } + } + return $val; +} + +public function MergeBlock($BlockLst,$SrcId='assigned',$Query='',$QryPrms=false) { + + if ($SrcId==='assigned') { + $Arg = array($BlockLst,&$SrcId,&$Query,&$QryPrms); + if (!$this->meth_Misc_Assign($BlockLst, $Arg, 'MergeBlock')) return 0; + $BlockLst = $Arg[0]; $SrcId = &$Arg[1]; $Query = &$Arg[2]; + } + + if (is_string($BlockLst)) $BlockLst = explode(',',$BlockLst); + + if ($SrcId==='cond') { + $Nbr = 0; + foreach ($BlockLst as $Block) { + $Block = trim($Block); + if ($Block!=='') $Nbr += $this->meth_Merge_AutoOn($this->Source,$Block,true,true); + } + return $Nbr; + } else { + return $this->meth_Merge_Block($this->Source,$BlockLst,$SrcId,$Query,false,0,$QryPrms); + } + +} + +public function MergeField($NameLst,$Value='assigned',$IsUserFct=false,$DefaultPrm=false) { + + $FctCheck = $IsUserFct; + if ($PlugIn = isset($this->_piOnMergeField)) $ArgPi = array('','',&$Value,0,&$this->Source,0,0); + $SubStart = 0; + $Ok = true; + $Prm = is_array($DefaultPrm); + + if ( ($Value==='assigned') && ($NameLst!=='var') && ($NameLst!=='onshow') && ($NameLst!=='onload') ) { + $Arg = array($NameLst,&$Value,&$IsUserFct,&$DefaultPrm); + if (!$this->meth_Misc_Assign($NameLst, $Arg, 'MergeField')) return false; + $NameLst = $Arg[0]; $Value = &$Arg[1]; $IsUserFct = &$Arg[2]; $DefaultPrm = &$Arg[3]; + } + + $NameLst = explode(',',$NameLst); + + foreach ($NameLst as $Name) { + $Name = trim($Name); + $Cont = false; + switch ($Name) { + case '': $Cont=true;break; + case 'onload': $this->meth_Merge_AutoOn($this->Source,'onload',true,true);$Cont=true;break; + case 'onshow': $this->meth_Merge_AutoOn($this->Source,'onshow',true,true);$Cont=true;break; + case 'var': $this->meth_Merge_AutoVar($this->Source,true);$Cont=true;break; + } + if ($Cont) continue; + if ($PlugIn) $ArgPi[0] = $Name; + $PosBeg = 0; + // Initilize the user function (only once) + if ($FctCheck) { + $FctInfo = $Value; + $ErrMsg = false; + if (!$this->meth_Misc_UserFctCheck($FctInfo,'f',$ErrMsg,$ErrMsg,false)) return $this->meth_Misc_Alert('with MergeField() method',$ErrMsg); + $FctArg = array('',''); + $SubStart = false; + $FctCheck = false; + } + while ($Loc = $this->meth_Locator_FindTbs($this->Source,$Name,$PosBeg,'.')) { + if ($Prm) $Loc->PrmLst = array_merge($DefaultPrm,$Loc->PrmLst); + // Apply user function + if ($IsUserFct) { + $FctArg[0] = &$Loc->SubName; $FctArg[1] = &$Loc->PrmLst; + $Value = call_user_func_array($FctInfo,$FctArg); + } + // Plug-ins + if ($PlugIn) { + $ArgPi[1] = $Loc->SubName; $ArgPi[3] = &$Loc->PrmLst; $ArgPi[5] = &$Loc->PosBeg; $ArgPi[6] = &$Loc->PosEnd; + $Ok = $this->meth_PlugIn_RunAll($this->_piOnMergeField,$ArgPi); + } + // Merge the field + if ($Ok) { + $PosBeg = $this->meth_Locator_Replace($this->Source,$Loc,$Value,$SubStart); + } else { + $PosBeg = $Loc->PosEnd; + } + } + } +} + +/** + * Replace a set of simple TBS fields (that is fields without any parameters) with more complexe TBS fields. + * @param array $fields An associative array of items to replace. + * Keys are the name of the simple field to replace. + * Values are the parameters of the field as an array or as a string. + * Parameter 'name' will be used as the new name of the field, by default it is the same name as the simple field. + * @param string $blockName (optional) The name of the block for prefixing fields. + */ +public function ReplaceFields($fields, $blockName = false) { + + $prefix = ($blockName) ? $blockName . '.' : ''; + + // calling the replace using array is faster than a loop + $what = array(); + $with = array(); + foreach ($fields as $name => $prms) { + $what[] = $this->_ChrOpen . $name . $this->_ChrClose; + if (is_array($prms)) { + // field replace + $lst = ''; + foreach ($prms as $p => $v) { + if ($p === 'name') { + $name = $v; + } else { + if ($v === true) { + $lst .= ';' . $p; + } elseif (is_array($v)) { + foreach($v as $x) { + $lst .= ';' . $p . '=' . $x; + } + } else { + $lst .= ';' . $p . '=' . $v; + } + } + } + $with[] = $this->_ChrOpen . $prefix . $name . $lst . $this->_ChrClose; + } else { + // simple string replace + $with[] = $prms; + } + } + + $this->Source = str_replace($what, $with, $this->Source); + +} + +public function Show($Render=false) { + $Ok = true; + if ($Render===false) $Render = $this->Render; + if ($this->_PlugIns_Ok) { + if (isset($this->_piBeforeShow) || isset($this->_piAfterShow)) { + // Plug-ins + $ArgLst = func_get_args(); + $ArgLst[0] = &$Render; + if (isset($this->_piBeforeShow)) $Ok = $this->meth_PlugIn_RunAll($this->_piBeforeShow,$ArgLst); + } + } + if ($Ok!==false) { + if ($this->OnShow) $this->meth_Merge_AutoOn($this->Source,'onshow',true,true); + $this->meth_Merge_AutoVar($this->Source,true); + } + if ($this->_PlugIns_Ok && isset($ArgLst) && isset($this->_piAfterShow)) $this->meth_PlugIn_RunAll($this->_piAfterShow,$ArgLst); + if ($this->_ErrMsgName!=='') $this->MergeField($this->_ErrMsgName, $this->ErrMsg); + if ($this->meth_Misc_IsMainTpl()) { + if (($Render & TBS_OUTPUT)==TBS_OUTPUT) echo $this->Source; + if (($Render & TBS_EXIT)==TBS_EXIT) exit; + } elseif ($this->OldSubTpl) { + if (($Render & TBS_OUTPUT)==TBS_OUTPUT) echo $this->Source; + } + return $Ok; +} + +public function PlugIn($Prm1,$Prm2=0) { + + if (is_numeric($Prm1)) { + switch ($Prm1) { + case TBS_INSTALL: // Try to install the plug-in + $PlugInId = $Prm2; + if (isset($this->_PlugIns[$PlugInId])) { + return $this->meth_Misc_Alert('with PlugIn() method','plug-in \''.$PlugInId.'\' is already installed.'); + } else { + $ArgLst = func_get_args(); + array_shift($ArgLst); array_shift($ArgLst); + return $this->meth_PlugIn_Install($PlugInId,$ArgLst,false); + } + case TBS_ISINSTALLED: // Check if the plug-in is installed + return isset($this->_PlugIns[$Prm2]); + case -4: // Deactivate special plug-ins + $this->_PlugIns_Ok_save = $this->_PlugIns_Ok; + $this->_PlugIns_Ok = false; + return true; + case -5: // Deactivate OnFormat + $this->_piOnFrm_Ok_save = $this->_piOnFrm_Ok; + $this->_piOnFrm_Ok = false; + return true; + case -10: // Restore + if (isset($this->_PlugIns_Ok_save)) $this->_PlugIns_Ok = $this->_PlugIns_Ok_save; + if (isset($this->_piOnFrm_Ok_save)) $this->_piOnFrm_Ok = $this->_piOnFrm_Ok_save; + return true; + } + + } elseif (is_string($Prm1)) { + // Plug-in's command + $p = strpos($Prm1,'.'); + if ($p===false) { + $PlugInId = $Prm1; + } else { + $PlugInId = substr($Prm1,0,$p); // direct command + } + if (!isset($this->_PlugIns[$PlugInId])) { + if (!$this->meth_PlugIn_Install($PlugInId,array(),true)) return false; + } + if (!isset($this->_piOnCommand[$PlugInId])) return $this->meth_Misc_Alert('with PlugIn() method','plug-in \''.$PlugInId.'\' can\'t run any command because the OnCommand event is not defined or activated.'); + $ArgLst = func_get_args(); + if ($p===false) array_shift($ArgLst); + $Ok = call_user_func_array($this->_piOnCommand[$PlugInId],$ArgLst); + if (is_null($Ok)) $Ok = true; + return $Ok; + } + return $this->meth_Misc_Alert('with PlugIn() method','\''.$Prm1.'\' is an invalid plug-in key, the type of the value is \''.gettype($Prm1).'\'.'); + +} + +// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*- + +function meth_Locator_FindTbs(&$Txt,$Name,$Pos,$ChrSub) { +// Find a TBS Locator + + $PosEnd = false; + $PosMax = strlen($Txt) -1; + $Start = $this->_ChrOpen.$Name; + + do { + // Search for the opening char + if ($Pos>$PosMax) return false; + $Pos = strpos($Txt,$Start,$Pos); + + // If found => next chars are analyzed + if ($Pos===false) { + return false; + } else { + $Loc = new clsTbsLocator; + $ReadPrm = false; + $PosX = $Pos + strlen($Start); + $x = $Txt[$PosX]; + + if ($x===$this->_ChrClose) { + $PosEnd = $PosX; + } elseif ($x===$ChrSub) { + $Loc->SubOk = true; // it is no longer the false value + $ReadPrm = true; + $PosX++; + } elseif (strpos(';',$x)!==false) { + $ReadPrm = true; + $PosX++; + } else { + $Pos++; + } + + $Loc->PosBeg = $Pos; + if ($ReadPrm) { + self::f_Loc_PrmRead($Txt,$PosX,false,'\'',$this->_ChrOpen,$this->_ChrClose,$Loc,$PosEnd); + if ($PosEnd===false) { + $this->meth_Misc_Alert('','can\'t found the end of the tag \''.substr($Txt,$Pos,$PosX-$Pos+10).'...\'.'); + $Pos++; + } else { + self::meth_Misc_ApplyPrmCombo($Loc->PrmLst, $Loc); + } + } + + } + + } while ($PosEnd===false); + + $Loc->PosEnd = $PosEnd; + if ($Loc->SubOk) { + $Loc->FullName = $Name.'.'.$Loc->SubName; + $Loc->SubLst = explode('.',$Loc->SubName); + $Loc->SubNbr = count($Loc->SubLst); + } else { + $Loc->FullName = $Name; + } + if ( $ReadPrm && ( isset($Loc->PrmLst['enlarge']) || isset($Loc->PrmLst['comm']) ) ) { + $Loc->PosBeg0 = $Loc->PosBeg; + $Loc->PosEnd0 = $Loc->PosEnd; + $enlarge = (isset($Loc->PrmLst['enlarge'])) ? $Loc->PrmLst['enlarge'] : $Loc->PrmLst['comm']; + if (($enlarge===true) || ($enlarge==='')) { + $Loc->Enlarged = self::f_Loc_EnlargeToStr($Txt,$Loc,''); + } else { + $Loc->Enlarged = self::f_Loc_EnlargeToTag($Txt,$Loc,$enlarge,false); + } + } + + return $Loc; + +} + +/** + * Note: keep the « & » if the function is called with it. + * + * @return object + */ +function meth_Locator_SectionNewBDef(&$LocR,$BlockName,$Txt,$PrmLst,$Cache) { + + $Chk = true; + $LocLst = array(); + $Pos = 0; + $Sort = false; + + if ($this->_PlugIns_Ok && isset($this->_piOnCacheField)) { + $pi = true; + $ArgLst = array(0=>$BlockName, 1=>false, 2=>&$Txt, 3=>array('att'=>true), 4=>&$LocLst, 5=>&$Pos); + } else { + $pi = false; + } + + // Cache TBS locators + $Cache = ($Cache && $this->TurboBlock); + if ($Cache) { + + $Chk = false; + while ($Loc = $this->meth_Locator_FindTbs($Txt,$BlockName,$Pos,'.')) { + + $LocNbr = 1 + count($LocLst); + $LocLst[$LocNbr] = &$Loc; + + // Next search position : always ("original PosBeg" + 1). + // Must be done here because loc can be moved by the plug-in. + if ($Loc->Enlarged) { + // Enlarged + $Pos = $Loc->PosBeg0 + 1; + $Loc->Enlarged = false; + } else { + // Normal + $Pos = $Loc->PosBeg + 1; + } + + // Note: the plug-in may move, delete and add one or several locs. + // Move : backward or forward (will be sorted) + // Delete : add property DelMe=true + // Add : at the end of $LocLst (will be sorted) + if ($pi) { + $ArgLst[1] = &$Loc; + $this->meth_Plugin_RunAll($this->_piOnCacheField,$ArgLst); + } + + if (($Loc->SubName==='#') || ($Loc->SubName==='$')) { + $Loc->IsRecInfo = true; + $Loc->RecInfo = $Loc->SubName; + $Loc->SubName = ''; + } else { + $Loc->IsRecInfo = false; + } + + // Process parameter att for new added locators. + $NewNbr = count($LocLst); + for ($i=$LocNbr;$i<=$NewNbr;$i++) { + $li = &$LocLst[$i]; + if (isset($li->PrmLst['att'])) { + $LocSrc = substr($Txt,$li->PosBeg,$li->PosEnd-$li->PosBeg+1); // for error message + if ($this->f_Xml_AttFind($Txt,$li,$LocLst,$this->AttDelim)) { + if (isset($Loc->PrmLst['atttrue'])) { + $li->PrmLst['magnet'] = '#'; + $li->PrmLst['ope'] = (isset($li->PrmLst['ope'])) ? $li->PrmLst['ope'].',attbool' : 'attbool'; + } + if ($i==$LocNbr) { + $Pos = $Loc->DelPos; + } + } else { + $this->meth_Misc_Alert('','TBS is not able to merge the field '.$LocSrc.' because the entity targeted by parameter \'att\' cannot be found.'); + } + } + } + + unset($Loc); + + } + + // Re-order loc + $e = self::f_Loc_Sort($LocLst, true, 1); + $Chk = ($e > 0); + + } + + // Create the object + $o = (object) null; + $o->Prm = $PrmLst; + $o->LocLst = $LocLst; + $o->LocNbr = count($LocLst); + $o->Name = $BlockName; + $o->Src = $Txt; + $o->Chk = $Chk; + $o->IsSerial = false; + $o->AutoSub = false; + $i = 1; + while (isset($PrmLst['sub'.$i])) { + $o->AutoSub = $i; + $i++; + } + + $LocR->BDefLst[] = &$o; // Can be usefull for plug-in + return $o; + +} + +/** + * Add a special section to the LocR. + * + * @param object $LocR + * @param string $BlockName + * @param object $BDef + * @param string $Field Name of the field on which the group of values is defined. + * @param string $FromPrm Parameter that induced the section. + * + * @return object + */ +function meth_Locator_MakeBDefFromField(&$LocR,$BlockName,$Field,$FromPrm) { + + if (strpos($Field,$this->_ChrOpen)===false) { + // The field is a simple colmun name + $src = $this->_ChrOpen.$BlockName.'.'.$Field.';tbstype='.$FromPrm.$this->_ChrClose; // tbstype is an internal parameter for catching errors + } else { + // The fields is a TBS field's expression + $src = $Field; + } + + $BDef = $this->meth_Locator_SectionNewBDef($LocR,$BlockName,$src,array(),true); + + if ($BDef->LocNbr==0) $this->meth_Misc_Alert('Parameter '.$FromPrm,'The value \''.$Field.'\' is unvalide for this parameter.'); + + return $BDef; + +} + +/** + * Add a special section to the LocR. + * + * @param object $LocR + * @param string $BlockName + * @param object $BDef + * @param string $Type Type of behavior: 'H' header, 'F' footer, 'S' splitter. + * @param string $Field Name of the field on which the group of values is defined. + * @param string $FromPrm Parameter that induced the section. + */ +function meth_Locator_SectionAddGrp(&$LocR,$BlockName,&$BDef,$Type,$Field,$FromPrm) { + + $BDef->PrevValue = false; + $BDef->Type = $Type; // property not used in native, but designed for plugins + + // Save sub items in a structure near to Locator. + $BDef->FDef = $this->meth_Locator_MakeBDefFromField($LocR,$BlockName,$Field,$FromPrm); + + if ($Type==='H') { + // Header behavior + if ($LocR->HeaderFound===false) { + $LocR->HeaderFound = true; + $LocR->HeaderNbr = 0; + $LocR->HeaderDef = array(); // 1 to HeaderNbr + } + $i = ++$LocR->HeaderNbr; + $LocR->HeaderDef[$i] = $BDef; + } else { + // Footer behavior (footer or splitter) + if ($LocR->FooterFound===false) { + $LocR->FooterFound = true; + $LocR->FooterNbr = 0; + $LocR->FooterDef = array(); // 1 to FooterNbr + } + $BDef->AddLastGrp = ($Type==='F'); + $i = ++$LocR->FooterNbr; + $LocR->FooterDef[$i] = $BDef; + } + +} + +/** + * Merge a locator with a text. + * + * @param string $Txt The source string to modify. + * @param object $Loc The locator of the field to replace. + * @param mixed $Value The value to merge with. + * @param integer|false $SubStart The offset of subname to considere. + * + * @return integer The position just after the replaced field. Or the position of the start if the replace is canceled. + * This position can be useful because we don't know in advance how $Value will be replaced. + * $Loc->PosNext is also set to the next search position when embedded fields are allowed. + */ +function meth_Locator_Replace(&$Txt,&$Loc,&$Value,$SubStart) { + + // Found the value if there is a subname + if (($SubStart!==false) && $Loc->SubOk) { + for ($i=$SubStart;$i<$Loc->SubNbr;$i++) { + $x = $Loc->SubLst[$i]; // &$Loc... brings an error with Event Example, I don't know why. + if (is_array($Value)) { + if (isset($Value[$x])) { + $Value = &$Value[$x]; + } elseif (array_key_exists($x,$Value)) {// can happens when value is NULL + $Value = &$Value[$x]; + } else { + if (!isset($Loc->PrmLst['noerr'])) $this->meth_Misc_Alert($Loc,'item \''.$x.'\' is not an existing key in the array.',true); + unset($Value); $Value = ''; break; + } + } elseif (is_object($Value)) { + $form = $this->f_Misc_ParseFctForm($x); + $n = $form['name']; + if ( method_exists($Value,$n) || ($form['as_fct'] && method_exists($Value,'__call')) ) { + if ($this->MethodsAllowed || !in_array(strtok($Loc->FullName,'.'),array('onload','onshow','var')) ) { + $x = call_user_func_array(array(&$Value,$n),$form['args']); + } else { + if (!isset($Loc->PrmLst['noerr'])) $this->meth_Misc_Alert($Loc,'\''.$n.'\' is a method and the current TBS settings do not allow to call methods on automatic fields.',true); + $x = ''; + } + } elseif (property_exists($Value,$n)) { + $x = &$Value->$n; + } elseif (isset($Value->$n)) { + $x = $Value->$n; // useful for overloaded property + } else { + if (!isset($Loc->PrmLst['noerr'])) $this->meth_Misc_Alert($Loc,'item '.$n.'\' is neither a method nor a property in the class \''.get_class($Value).'\'. Overloaded properties must also be available for the __isset() magic method.',true); + unset($Value); $Value = ''; break; + } + $Value = &$x; unset($x); $x = ''; + } else { + if (!isset($Loc->PrmLst['noerr'])) $this->meth_Misc_Alert($Loc,'item before \''.$x.'\' is neither an object nor an array. Its type is '.gettype($Value).'.',true); + unset($Value); $Value = ''; break; + } + } + } + + $CurrVal = $Value; // Unlink + + if (isset($Loc->PrmLst['onformat'])) { + if ($Loc->FirstMerge) { + $Loc->OnFrmInfo = $Loc->PrmLst['onformat']; + $Loc->OnFrmArg = array($Loc->FullName,'',&$Loc->PrmLst,&$this); + $ErrMsg = false; + if (!$this->meth_Misc_UserFctCheck($Loc->OnFrmInfo,'f',$ErrMsg,$ErrMsg,true)) { + unset($Loc->PrmLst['onformat']); + if (!isset($Loc->PrmLst['noerr'])) $this->meth_Misc_Alert($Loc,'(parameter onformat) '.$ErrMsg); + $Loc->OnFrmInfo = false; + } + } else { + $Loc->OnFrmArg[3] = &$this; // bugs.php.net/51174 + } + if ($Loc->OnFrmInfo !== false) { + $Loc->OnFrmArg[1] = &$CurrVal; + if (isset($Loc->PrmLst['subtpl'])) { + $this->meth_Misc_ChangeMode(true,$Loc,$CurrVal); + call_user_func_array($Loc->OnFrmInfo,$Loc->OnFrmArg); + $this->meth_Misc_ChangeMode(false,$Loc,$CurrVal); + $Loc->ConvProtect = false; + $Loc->ConvStr = false; + } else { + call_user_func_array($Loc->OnFrmInfo,$Loc->OnFrmArg); + } + } + } + + if ($Loc->FirstMerge) { + if (isset($Loc->PrmLst['frm'])) { + $Loc->ConvMode = 0; // Frm + $Loc->ConvProtect = false; + } else { + // Analyze parameter 'strconv' + if (isset($Loc->PrmLst['strconv'])) { + $this->meth_Conv_Prepare($Loc, $Loc->PrmLst['strconv']); + } elseif (isset($Loc->PrmLst['htmlconv'])) { // compatibility + $this->meth_Conv_Prepare($Loc, $Loc->PrmLst['htmlconv']); + } else { + if ($this->Charset===false) $Loc->ConvStr = false; // No conversion + } + // Analyze parameter 'protect' + if (isset($Loc->PrmLst['protect'])) { + $x = strtolower($Loc->PrmLst['protect']); + if ($x==='no') { + $Loc->ConvProtect = false; + } elseif ($x==='yes') { + $Loc->ConvProtect = true; + } + } elseif ($this->Protect===false) { + $Loc->ConvProtect = false; + } + } + if ($Loc->Ope = isset($Loc->PrmLst['ope'])) { + $OpeLst = explode(',',$Loc->PrmLst['ope']); + $Loc->OpeAct = array(); + $Loc->OpeArg = array(); + $Loc->OpeUtf8 = false; + foreach ($OpeLst as $i=>$ope) { + if ($ope==='list') { + $Loc->OpeAct[$i] = 1; + $Loc->OpePrm[$i] = (isset($Loc->PrmLst['valsep'])) ? $Loc->PrmLst['valsep'] : ','; + if (($Loc->ConvMode===1) && $Loc->ConvStr) $Loc->ConvMode = -1; // special mode for item list conversion + } elseif ($ope==='minv') { + $Loc->OpeAct[$i] = 11; + $Loc->MSave = $Loc->MagnetId; + } elseif ($ope==='attbool') { // this operation key is set when a loc is cached with paremeter atttrue + $Loc->OpeAct[$i] = 14; + } elseif ($ope==='utf8') { $Loc->OpeUtf8 = true; + } elseif ($ope==='upper') { $Loc->OpeAct[$i] = 15; + } elseif ($ope==='lower') { $Loc->OpeAct[$i] = 16; + } elseif ($ope==='upper1') { $Loc->OpeAct[$i] = 17; + } elseif ($ope==='upperw') { $Loc->OpeAct[$i] = 18; + } else { + $x = substr($ope,0,4); + if ($x==='max:') { + $Loc->OpeAct[$i] = (isset($Loc->PrmLst['maxhtml'])) ? 2 : 3; + if (isset($Loc->PrmLst['maxutf8'])) $Loc->OpeUtf8 = true; + $Loc->OpePrm[$i] = intval(trim(substr($ope,4))); + $Loc->OpeEnd = (isset($Loc->PrmLst['maxend'])) ? $Loc->PrmLst['maxend'] : '...'; + if ($Loc->OpePrm[$i]<=0) $Loc->Ope = false; + } elseif ($x==='mod:') {$Loc->OpeAct[$i] = 5; $Loc->OpePrm[$i] = '0'+trim(substr($ope,4)); + } elseif ($x==='add:') {$Loc->OpeAct[$i] = 6; $Loc->OpePrm[$i] = '0'+trim(substr($ope,4)); + } elseif ($x==='mul:') {$Loc->OpeAct[$i] = 7; $Loc->OpePrm[$i] = '0'+trim(substr($ope,4)); + } elseif ($x==='div:') {$Loc->OpeAct[$i] = 8; $Loc->OpePrm[$i] = '0'+trim(substr($ope,4)); + } elseif ($x==='mok:') {$Loc->OpeAct[$i] = 9; $Loc->OpeMOK[] = trim(substr($ope,4)); $Loc->MSave = $Loc->MagnetId; + } elseif ($x==='mko:') {$Loc->OpeAct[$i] =10; $Loc->OpeMKO[] = trim(substr($ope,4)); $Loc->MSave = $Loc->MagnetId; + } elseif ($x==='nif:') {$Loc->OpeAct[$i] =12; $Loc->OpePrm[$i] = trim(substr($ope,4)); + } elseif ($x==='msk:') {$Loc->OpeAct[$i] =13; $Loc->OpePrm[$i] = trim(substr($ope,4)); + } elseif (isset($this->_piOnOperation)) { + $Loc->OpeAct[$i] = 0; + $Loc->OpePrm[$i] = $ope; + $Loc->OpeArg[$i] = array($Loc->FullName,&$CurrVal,&$Loc->PrmLst,&$Txt,$Loc->PosBeg,$Loc->PosEnd,&$Loc); + $Loc->PrmLst['_ope'] = $Loc->PrmLst['ope']; + } elseif (!isset($Loc->PrmLst['noerr'])) { + $this->meth_Misc_Alert($Loc,'parameter ope doesn\'t support value \''.$ope.'\'.',true); + } + } + } + } + $Loc->FirstMerge = false; + } + $ConvProtect = $Loc->ConvProtect; + + // Plug-in OnFormat + if ($this->_piOnFrm_Ok) { + if (isset($Loc->OnFrmArgPi)) { + $Loc->OnFrmArgPi[1] = &$CurrVal; + $Loc->OnFrmArgPi[3] = &$this; // bugs.php.net/51174 + } else { + $Loc->OnFrmArgPi = array($Loc->FullName,&$CurrVal,&$Loc->PrmLst,&$this); + } + $this->meth_PlugIn_RunAll($this->_piOnFormat,$Loc->OnFrmArgPi); + } + + // Operation + if ($Loc->Ope) { + foreach ($Loc->OpeAct as $i=>$ope) { + switch ($ope) { + case 0: + $Loc->PrmLst['ope'] = $Loc->OpePrm[$i]; // for compatibility + $OpeArg = &$Loc->OpeArg[$i]; + $OpeArg[1] = &$CurrVal; $OpeArg[3] = &$Txt; + if (!$this->meth_PlugIn_RunAll($this->_piOnOperation,$OpeArg)) { + $Loc->PosNext = $Loc->PosBeg + 1; // +1 in order to avoid infinit loop + return $Loc->PosNext; + } + break; + case 1: + if ($Loc->ConvMode===-1) { + if (is_array($CurrVal)) { + foreach ($CurrVal as $k=>$v) { + $v = $this->meth_Misc_ToStr($v); + $this->meth_Conv_Str($v,$Loc->ConvBr); + $CurrVal[$k] = $v; + } + $CurrVal = implode($Loc->OpePrm[$i],$CurrVal); + } else { + $CurrVal = $this->meth_Misc_ToStr($CurrVal); + $this->meth_Conv_Str($CurrVal,$Loc->ConvBr); + } + } else { + if (is_array($CurrVal)) $CurrVal = implode($Loc->OpePrm[$i],$CurrVal); + } + break; + case 2: + $x = $this->meth_Misc_ToStr($CurrVal); + if (strlen($x)>$Loc->OpePrm[$i]) { + $this->f_Xml_Max($x,$Loc->OpePrm[$i],$Loc->OpeEnd); + } + break; + case 3: + $x = $this->meth_Misc_ToStr($CurrVal); + if (strlen($x)>$Loc->OpePrm[$i]) { + if ($Loc->OpeUtf8) { + $CurrVal = mb_substr($x,0,$Loc->OpePrm[$i],'UTF-8').$Loc->OpeEnd; + } else { + $CurrVal = substr($x,0,$Loc->OpePrm[$i]).$Loc->OpeEnd; + } + } + break; + case 5: $CurrVal = ('0'+$CurrVal) % $Loc->OpePrm[$i]; break; + case 6: $CurrVal = ('0'+$CurrVal) + $Loc->OpePrm[$i]; break; + case 7: $CurrVal = ('0'+$CurrVal) * $Loc->OpePrm[$i]; break; + case 8: $CurrVal = ('0'+$CurrVal) / $Loc->OpePrm[$i]; break; + case 9; case 10: + if ($ope===9) { + $CurrVal = (in_array($this->meth_Misc_ToStr($CurrVal),$Loc->OpeMOK)) ? ' ' : ''; + } else { + $CurrVal = (in_array($this->meth_Misc_ToStr($CurrVal),$Loc->OpeMKO)) ? '' : ' '; + } // no break here + case 11: + if ($this->meth_Misc_ToStr($CurrVal)==='') { + if ($Loc->MagnetId===0) $Loc->MagnetId = $Loc->MSave; + } else { + if ($Loc->MagnetId!==0) { + $Loc->MSave = $Loc->MagnetId; + $Loc->MagnetId = 0; + } + $CurrVal = ''; + } + break; + case 12: if ($this->meth_Misc_ToStr($CurrVal)===$Loc->OpePrm[$i]) $CurrVal = ''; break; + case 13: $CurrVal = str_replace('*',$CurrVal,$Loc->OpePrm[$i]); break; + case 14: $CurrVal = self::f_Loc_AttBoolean($CurrVal, $Loc->PrmLst['atttrue'], $Loc->AttName); break; + case 15: $CurrVal = ($Loc->OpeUtf8) ? mb_convert_case($CurrVal, MB_CASE_UPPER, 'UTF-8') : strtoupper($CurrVal); break; + case 16: $CurrVal = ($Loc->OpeUtf8) ? mb_convert_case($CurrVal, MB_CASE_LOWER, 'UTF-8') : strtolower($CurrVal); break; + case 17: $CurrVal = ucfirst($CurrVal); break; + case 18: $CurrVal = ($Loc->OpeUtf8) ? mb_convert_case($CurrVal, MB_CASE_TITLE, 'UTF-8') : ucwords(strtolower($CurrVal)); break; + } + } + } + + // String conversion or format + if ($Loc->ConvMode===1) { // Usual string conversion + $CurrVal = $this->meth_Misc_ToStr($CurrVal); + if ($Loc->ConvStr) $this->meth_Conv_Str($CurrVal,$Loc->ConvBr); + } elseif ($Loc->ConvMode===0) { // Format + $CurrVal = $this->meth_Misc_Format($CurrVal,$Loc->PrmLst); + } elseif ($Loc->ConvMode===2) { // Special string conversion + $CurrVal = $this->meth_Misc_ToStr($CurrVal); + if ($Loc->ConvStr) $this->meth_Conv_Str($CurrVal,$Loc->ConvBr); + if ($Loc->ConvEsc) $CurrVal = str_replace('\'','\'\'',$CurrVal); + if ($Loc->ConvWS) { + $check = ' '; + $nbsp = ' '; + do { + $pos = strpos($CurrVal,$check); + if ($pos!==false) $CurrVal = substr_replace($CurrVal,$nbsp,$pos,1); + } while ($pos!==false); + } + if ($Loc->ConvJS) { + $CurrVal = addslashes($CurrVal); // apply to ('), ("), (\) and (null) + $CurrVal = str_replace(array("\n","\r","\t"),array('\n','\r','\t'),$CurrVal); + } + if ($Loc->ConvUrl) $CurrVal = urlencode($CurrVal); + if ($Loc->ConvUtf8) $CurrVal = iconv('ISO-8859-1', 'UTF-8', $CurrVal); + } + + // if/then/else process, there may be several if/then + if ($Loc->PrmIfNbr) { + $z = false; + $i = 1; + while ($i!==false) { + if ($Loc->PrmIfVar[$i]) $Loc->PrmIfVar[$i] = $this->meth_Merge_AutoVar($Loc->PrmIf[$i],true); + $x = str_replace($this->_ChrVal,$CurrVal,$Loc->PrmIf[$i]); + if ($this->f_Misc_CheckCondition($x)) { + if (isset($Loc->PrmThen[$i])) { + if ($Loc->PrmThenVar[$i]) $Loc->PrmThenVar[$i] = $this->meth_Merge_AutoVar($Loc->PrmThen[$i],true); + $z = $Loc->PrmThen[$i]; + } + $i = false; + } else { + $i++; + if ($i>$Loc->PrmIfNbr) { + if (isset($Loc->PrmLst['else'])) { + if ($Loc->PrmElseVar) $Loc->PrmElseVar = $this->meth_Merge_AutoVar($Loc->PrmLst['else'],true); + $z =$Loc->PrmLst['else']; + } + $i = false; + } + } + } + if ($z!==false) { + if ($ConvProtect) { + $CurrVal = str_replace($this->_ChrOpen,$this->_ChrProtect,$CurrVal); // TBS protection + $ConvProtect = false; + } + $CurrVal = str_replace($this->_ChrVal,$CurrVal,$z); + } + } + + $IsTpl = false; // Indicates is $CurrVal is a sub-template + + if (isset($Loc->PrmLst['file'])) { + $x = $Loc->PrmLst['file']; + if ($x===true) $x = $CurrVal; + $this->meth_Merge_AutoVar($x,false); + $x = trim(str_replace($this->_ChrVal,$CurrVal,$x)); + $CurrVal = ''; + if ($x!=='') { + if ($this->f_Misc_GetFile($CurrVal, $x, $this->_LastFile, $this->IncludePath)) { + $this->meth_Locator_PartAndRename($CurrVal, $Loc->PrmLst); + $IsTpl = true; + } else { + if (!isset($Loc->PrmLst['noerr'])) $this->meth_Misc_Alert($Loc,'the file \''.$x.'\' given by parameter file is not found or not readable.',true); + } + $ConvProtect = false; + } + } + + if (isset($Loc->PrmLst['script'])) {// Include external PHP script + $x = $Loc->PrmLst['script']; + if ($this->ScriptsAllowed) { + if ($x===true) $x = $CurrVal; + $this->meth_Merge_AutoVar($x,false); + $x = trim(str_replace($this->_ChrVal,$CurrVal,$x)); + if (basename($x) == basename($this->_LastFile)) { + if (!isset($Loc->PrmLst['noerr'])) $this->meth_Misc_Alert($Loc,'the file \''.$x.'\' given by parameter script cannot be called because it has the same name as the current template and this is suspicious.',true); + $x= ''; + } + } else { + if (!isset($Loc->PrmLst['noerr'])) $this->meth_Misc_Alert($Loc,'parameter \'script\' is forbidden by default. It can be allowed by a TBS option.',true); + $x = ''; + } + if ($x!=='') { + $this->_Subscript = $x; + $this->CurrPrm = &$Loc->PrmLst; + $sub = isset($Loc->PrmLst['subtpl']); + if ($sub) $this->meth_Misc_ChangeMode(true,$Loc,$CurrVal); + if ($this->meth_Misc_RunSubscript($CurrVal,$Loc->PrmLst)===false) { + if (!isset($Loc->PrmLst['noerr'])) $this->meth_Misc_Alert($Loc,'the file \''.$x.'\' given by parameter script is not found or not readable.',true); + } + if ($sub) $this->meth_Misc_ChangeMode(false,$Loc,$CurrVal); + $this->meth_Locator_PartAndRename($CurrVal, $Loc->PrmLst); + $IsTpl = true; + unset($this->CurrPrm); + $ConvProtect = false; + } + } + + if (isset($Loc->PrmLst['att'])) { + $this->f_Xml_AttFind($Txt,$Loc,true,$this->AttDelim); + if (isset($Loc->PrmLst['atttrue'])) { + $CurrVal = self::f_Loc_AttBoolean($CurrVal, $Loc->PrmLst['atttrue'], $Loc->AttName); + $Loc->PrmLst['magnet'] = '#'; + } + } + + // Case when it's an empty string + if ($CurrVal==='') { + + if ($Loc->MagnetId===false) { + if (isset($Loc->PrmLst['.'])) { + $Loc->MagnetId = -1; + } elseif (isset($Loc->PrmLst['ifempty'])) { + $Loc->MagnetId = -2; + } elseif (isset($Loc->PrmLst['magnet'])) { + $Loc->MagnetId = 1; + $Loc->PosBeg0 = $Loc->PosBeg; + $Loc->PosEnd0 = $Loc->PosEnd; + if ($Loc->PrmLst['magnet']==='#') { + if (!isset($Loc->AttBeg)) { + $Loc->PrmLst['att'] = '.'; + // no moving because att info would be modified and thus become wrong regarding to the eventually cached source + $this->f_Xml_AttFind($Txt,$Loc,false,$this->AttDelim); + } + if (isset($Loc->AttBeg)) { + $Loc->MagnetId = -3; + } else { + $this->meth_Misc_Alert($Loc,'parameter \'magnet=#\' cannot be processed because the corresponding attribute is not found.',true); + } + } elseif (isset($Loc->PrmLst['mtype'])) { + switch ($Loc->PrmLst['mtype']) { + case 'm+m': $Loc->MagnetId = 2; break; + case 'm*': $Loc->MagnetId = 3; break; + case '*m': $Loc->MagnetId = 4; break; + } + } + } elseif (isset($Loc->PrmLst['attadd'])) { + // In order to delete extra space + $Loc->PosBeg0 = $Loc->PosBeg; + $Loc->PosEnd0 = $Loc->PosEnd; + $Loc->MagnetId = 5; + } else { + $Loc->MagnetId = 0; + } + } + + switch ($Loc->MagnetId) { + case 0: break; + case -1: $CurrVal = ' '; break; // Enables to avoid null cells in HTML tables + case -2: $CurrVal = $Loc->PrmLst['ifempty']; break; + case -3: + // magnet=# + $Loc->Enlarged = true; + $Loc->PosBeg = ($Txt[$Loc->AttBeg-1]===' ') ? $Loc->AttBeg-1 : $Loc->AttBeg; + $Loc->PosEnd = $Loc->AttEnd; + break; + case 1: + $Loc->Enlarged = true; + $this->f_Loc_EnlargeToTag($Txt,$Loc,$Loc->PrmLst['magnet'],false); + break; + case 2: + $Loc->Enlarged = true; + $CurrVal = $this->f_Loc_EnlargeToTag($Txt,$Loc,$Loc->PrmLst['magnet'],true); + break; + case 3: + $Loc->Enlarged = true; + $Loc2 = $this->f_Xml_FindTag($Txt,$Loc->PrmLst['magnet'],true,$Loc->PosBeg,false,false,false); + if ($Loc2!==false) { + $Loc->PosBeg = $Loc2->PosBeg; + if ($Loc->PosEnd<$Loc2->PosEnd) $Loc->PosEnd = $Loc2->PosEnd; + } + break; + case 4: + $Loc->Enlarged = true; + $Loc2 = $this->f_Xml_FindTag($Txt,$Loc->PrmLst['magnet'],true,$Loc->PosBeg,true,false,false); + if ($Loc2!==false) $Loc->PosEnd = $Loc2->PosEnd; + break; + case 5: + $Loc->Enlarged = true; + if (substr($Txt,$Loc->PosBeg-1,1)==' ') $Loc->PosBeg--; + break; + } + $NewEnd = $Loc->PosBeg; // Useful when mtype='m+m' + } else { + + if ($ConvProtect) $CurrVal = str_replace($this->_ChrOpen,$this->_ChrProtect,$CurrVal); // TBS protection + $NewEnd = $Loc->PosBeg + ($IsTpl ? 0 : strlen($CurrVal)); + + } + + $Txt = substr_replace($Txt,$CurrVal,$Loc->PosBeg,$Loc->PosEnd-$Loc->PosBeg+1); + + $Loc->PosNext = $NewEnd; + return $NewEnd; // Return the new end position of the field + +} + +/** + * Return the first block locator just after the PosBeg position + * + * @param integer $Mode + * 1 : Merge_Auto => doesn't save $Loc->BlockSrc, save the bounds of TBS Def tags instead, return also fields + * 2 : FindBlockLst or GetBlockSource => save $Loc->BlockSrc without TBS Def tags + * 3 : GetBlockSource => save $Loc->BlockSrc with TBS Def tags + */ +function meth_Locator_FindBlockNext(&$Txt,$BlockName,$PosBeg,$ChrSub,$Mode,&$P1,&$FieldBefore) { + + $SearchDef = true; + $FirstField = false; + // Search for the first tag with parameter "block" + while ($SearchDef && ($Loc = $this->meth_Locator_FindTbs($Txt,$BlockName,$PosBeg,$ChrSub))) { + if (isset($Loc->PrmLst['block'])) { + if (isset($Loc->PrmLst['p1'])) { + if ($P1) return false; + $P1 = true; + } + $Block = $Loc->PrmLst['block']; + $SearchDef = false; + } elseif ($Mode===1) { + return $Loc; + } elseif ($FirstField===false) { + $FirstField = $Loc; + } + $PosBeg = $Loc->PosEnd; + } + + if ($SearchDef) { + if ($FirstField!==false) $FieldBefore = true; + return false; + } + + $Loc->PosDefBeg = -1; + + if ($Block==='begin') { // Block definied using begin/end + + if (($FirstField!==false) && ($FirstField->PosEnd<$Loc->PosBeg)) $FieldBefore = true; + + $Opened = 1; + while ($Loc2 = $this->meth_Locator_FindTbs($Txt,$BlockName,$PosBeg,$ChrSub)) { + if (isset($Loc2->PrmLst['block'])) { + switch ($Loc2->PrmLst['block']) { + case 'end': $Opened--; break; + case 'begin': $Opened++; break; + } + if ($Opened==0) { + if ($Mode===1) { + $Loc->PosBeg2 = $Loc2->PosBeg; + $Loc->PosEnd2 = $Loc2->PosEnd; + } else { + if ($Mode===2) { + $Loc->BlockSrc = substr($Txt,$Loc->PosEnd+1,$Loc2->PosBeg-$Loc->PosEnd-1); + } else { + $Loc->BlockSrc = substr($Txt,$Loc->PosBeg,$Loc2->PosEnd-$Loc->PosBeg+1); + } + $Loc->PosEnd = $Loc2->PosEnd; + } + $Loc->BlockFound = true; + return $Loc; + } + } + $PosBeg = $Loc2->PosEnd; + } + + return $this->meth_Misc_Alert($Loc,'a least one tag with parameter \'block=end\' is missing.',false,'in block\'s definition'); + + } + + if ($Mode===1) { + $Loc->PosBeg2 = false; + } else { + $beg = $Loc->PosBeg; + $end = $Loc->PosEnd; + if ($this->f_Loc_EnlargeToTag($Txt,$Loc,$Block,false)===false) return $this->meth_Misc_Alert($Loc,'at least one tag corresponding to '.$Loc->PrmLst['block'].' is not found. Check opening tags, closing tags and embedding levels.',false,'in block\'s definition'); + if ($Loc->SubOk || ($Mode===3)) { + $Loc->BlockSrc = substr($Txt,$Loc->PosBeg,$Loc->PosEnd-$Loc->PosBeg+1); + $Loc->PosDefBeg = $beg - $Loc->PosBeg; + $Loc->PosDefEnd = $end - $Loc->PosBeg; + } else { + $Loc->BlockSrc = substr($Txt,$Loc->PosBeg,$beg-$Loc->PosBeg).substr($Txt,$end+1,$Loc->PosEnd-$end); + } + } + + $Loc->BlockFound = true; + if (($FirstField!==false) && ($FirstField->PosEnd<$Loc->PosBeg)) $FieldBefore = true; + return $Loc; // methods return by ref by default + +} + +function meth_Locator_PartAndRename(&$CurrVal, &$PrmLst) { + + // Store part + if (isset($PrmLst['store'])) { + $storename = (isset($PrmLst['storename'])) ? $PrmLst['storename'] : 'default'; + if (!isset($this->TplStore[$storename])) $this->TplStore[$storename] = ''; + $this->TplStore[$storename] .= $this->f_Xml_GetPart($CurrVal, $PrmLst['store'], false); + } + + // Get part + if (isset($PrmLst['getpart'])) { + $part = $PrmLst['getpart']; + } elseif (isset($PrmLst['getbody'])) { + $part = $PrmLst['getbody']; + } else { + $part = false; + } + if ($part!=false) { + $CurrVal = $this->f_Xml_GetPart($CurrVal, $part, true); + } + + // Rename or delete TBS tags names + if (isset($PrmLst['rename'])) { + + $Replace = $PrmLst['rename']; + + if (is_string($Replace)) $Replace = explode(',',$Replace); + foreach ($Replace as $x) { + if (is_string($x)) $x = explode('=', $x); + if (count($x)==2) { + $old = trim($x[0]); + $new = trim($x[1]); + if ($old!=='') { + if ($new==='') { + $q = false; + $s = 'clear'; + $this->meth_Merge_Block($CurrVal, $old, $s, $q, false, false, false); + } else { + $old = $this->_ChrOpen.$old; + $old = array($old.'.', $old.' ', $old.';', $old.$this->_ChrClose); + $new = $this->_ChrOpen.$new; + $new = array($new.'.', $new.' ', $new.';', $new.$this->_ChrClose); + $CurrVal = str_replace($old,$new,$CurrVal); + } + } + } + } + + } + +} + +/** + * Retrieve the list of all sections and their finition for a given block name. + * + * @param string $Txt + * @param string $BlockName + * @param integer $Pos + * @param string|false $SpePrm The parameter's name for Special section (used for navigation bar), or false if none. + * + * @return object + */ +function meth_Locator_FindBlockLst(&$Txt,$BlockName,$Pos,$SpePrm) { +// Return a locator object covering all block definitions, even if there is no block definition found. + + $LocR = new clsTbsLocator; + $LocR->P1 = false; + $LocR->FieldOutside = false; + $LocR->FOStop = false; + $LocR->BDefLst = array(); + + $LocR->NoData = false; + $LocR->Special = false; + $LocR->HeaderFound = false; + $LocR->FooterFound = false; + $LocR->SerialEmpty = false; + $LocR->GrpBreak = false; // Only for plug-ins + + $LocR->BoundFound = false; + $LocR->CheckNext = false; + $LocR->CheckPrev = false; + + $LocR->WhenFound = false; + $LocR->WhenDefault = false; + + $LocR->SectionNbr = 0; // Normal sections + $LocR->SectionLst = array(); // 1 to SectionNbr + + $BDef = false; + $ParentLst = array(); + $Pid = 0; + + do { + + if ($BlockName==='') { + $Loc = false; + } else { + $Loc = $this->meth_Locator_FindBlockNext($Txt,$BlockName,$Pos,'.',2,$LocR->P1,$LocR->FieldOutside); + } + + if ($Loc===false) { + + if ($Pid>0) { // parentgrp mode => disconnect $Txt from the source + $Parent = &$ParentLst[$Pid]; + $Src = $Txt; + $Txt = &$Parent->Txt; + if ($LocR->BlockFound) { + // Redefine the Header block + $Parent->Src = substr($Src,0,$LocR->PosBeg); + // Add a Footer block + $BDef = $this->meth_Locator_SectionNewBDef($LocR,$BlockName,substr($Src,$LocR->PosEnd+1),$Parent->Prm,true); + $this->meth_Locator_SectionAddGrp($LocR,$BlockName,$BDef,'F',$Parent->Fld,'parentgrp'); + } + // Now go down to previous level + $Pos = $Parent->Pos; + $LocR->PosBeg = $Parent->Beg; + $LocR->PosEnd = $Parent->End; + $LocR->BlockFound = true; + unset($Parent); + unset($ParentLst[$Pid]); + $Pid--; + $Loc = true; + } + + } else { + + $Pos = $Loc->PosEnd; + + // Define the block limits + if ($LocR->BlockFound) { + if ( $LocR->PosBeg > $Loc->PosBeg ) $LocR->PosBeg = $Loc->PosBeg; + if ( $LocR->PosEnd < $Loc->PosEnd ) $LocR->PosEnd = $Loc->PosEnd; + } else { + $LocR->BlockFound = true; + $LocR->PosBeg = $Loc->PosBeg; + $LocR->PosEnd = $Loc->PosEnd; + } + + // Merge block parameters + if (count($Loc->PrmLst)>0) $LocR->PrmLst = array_merge($LocR->PrmLst,$Loc->PrmLst); + + // Force dynamic parameter to be cachable + if ($Loc->PosDefBeg>=0) { + $dynprm = array('when','headergrp','footergrp','parentgrp'); + foreach($dynprm as $dp) { + $n = 0; + if ((isset($Loc->PrmLst[$dp])) && (strpos($Loc->PrmLst[$dp],$this->_ChrOpen.$BlockName)!==false)) { + $n++; + if ($n==1) { + $len = $Loc->PosDefEnd - $Loc->PosDefBeg + 1; + $x = substr($Loc->BlockSrc,$Loc->PosDefBeg,$len); + } + $x = str_replace($Loc->PrmLst[$dp],'',$x); + } + if ($n>0) $Loc->BlockSrc = substr_replace($Loc->BlockSrc,$x,$Loc->PosDefBeg,$len); + } + } + // Save the block and cache its tags + $IsParentGrp = isset($Loc->PrmLst['parentgrp']); + $BDef = $this->meth_Locator_SectionNewBDef($LocR,$BlockName,$Loc->BlockSrc,$Loc->PrmLst,!$IsParentGrp); + + // Bounds + $BoundPrm = false; + $lst = array('firstingrp'=>1, 'lastingrp'=>2, 'singleingrp'=>3); // 1=prev, 2=next, 3=1+2=prev+next + foreach ($lst as $prm => $chk) { + if (isset($Loc->PrmLst[$prm])) { + $BoundPrm = $prm; + $BoundChk = $chk; + } + } + + // Add the text in the list of blocks + if (isset($Loc->PrmLst['nodata'])) { // Nodata section + $LocR->NoData = $BDef; + } elseif (($SpePrm!==false) && isset($Loc->PrmLst[$SpePrm])) { // Special section (used for navigation bar) + $LocR->Special = $BDef; + } elseif (isset($Loc->PrmLst['when'])) { + if ($LocR->WhenFound===false) { + $LocR->WhenFound = true; + $LocR->WhenSeveral = false; + $LocR->WhenNbr = 0; + $LocR->WhenLst = array(); + } + $this->meth_Merge_AutoVar($Loc->PrmLst['when'],false); + $BDef->WhenCond = $this->meth_Locator_SectionNewBDef($LocR,$BlockName,$Loc->PrmLst['when'],array(),true); + $BDef->WhenBeforeNS = ($LocR->SectionNbr===0); // position of the When section relativley to the Normal Section + $i = ++$LocR->WhenNbr; + $LocR->WhenLst[$i] = $BDef; + if (isset($Loc->PrmLst['several'])) $LocR->WhenSeveral = true; + } elseif (isset($Loc->PrmLst['default'])) { + $LocR->WhenDefault = $BDef; + $LocR->WhenDefaultBeforeNS = ($LocR->SectionNbr===0); + } elseif (isset($Loc->PrmLst['headergrp'])) { + $this->meth_Locator_SectionAddGrp($LocR,$BlockName,$BDef,'H',$Loc->PrmLst['headergrp'],'headergrp'); + } elseif (isset($Loc->PrmLst['footergrp'])) { + $this->meth_Locator_SectionAddGrp($LocR,$BlockName,$BDef,'F',$Loc->PrmLst['footergrp'],'footergrp'); + } elseif (isset($Loc->PrmLst['splittergrp'])) { + $this->meth_Locator_SectionAddGrp($LocR,$BlockName,$BDef,'S',$Loc->PrmLst['splittergrp'],'splittergrp'); + } elseif ($IsParentGrp) { + $this->meth_Locator_SectionAddGrp($LocR,$BlockName,$BDef,'H',$Loc->PrmLst['parentgrp'],'parentgrp'); + $BDef->Fld = $Loc->PrmLst['parentgrp']; + $BDef->Txt = &$Txt; + $BDef->Pos = $Pos; + $BDef->Beg = $LocR->PosBeg; + $BDef->End = $LocR->PosEnd; + $Pid++; + $ParentLst[$Pid] = $BDef; + $Txt = &$BDef->Src; + $Pos = $Loc->PosDefBeg + 1; + $LocR->BlockFound = false; + $LocR->PosBeg = false; + $LocR->PosEnd = false; + } elseif (isset($Loc->PrmLst['serial'])) { + // Section with serial subsections + $SrSrc = &$BDef->Src; + // Search the empty item + if ($LocR->SerialEmpty===false) { + $SrName = $BlockName.'_0'; + $x = false; + $SrLoc = $this->meth_Locator_FindBlockNext($SrSrc,$SrName,0,'.',2,$x,$x); + if ($SrLoc!==false) { + $LocR->SerialEmpty = $SrLoc->BlockSrc; + $SrSrc = substr_replace($SrSrc,'',$SrLoc->PosBeg,$SrLoc->PosEnd-$SrLoc->PosBeg+1); + } + } + $SrName = $BlockName.'_1'; + $x = false; + $SrLoc = $this->meth_Locator_FindBlockNext($SrSrc,$SrName,0,'.',2,$x,$x); + if ($SrLoc!==false) { + $SrId = 1; + do { + // Save previous subsection + $SrBDef = $this->meth_Locator_SectionNewBDef($LocR,$SrName,$SrLoc->BlockSrc,$SrLoc->PrmLst,true); + $SrBDef->SrBeg = $SrLoc->PosBeg; + $SrBDef->SrLen = $SrLoc->PosEnd - $SrLoc->PosBeg + 1; + $SrBDef->SrTxt = false; + $BDef->SrBDefLst[$SrId] = $SrBDef; + // Put in order + $BDef->SrBDefOrdered[$SrId] = $SrBDef; + $i = $SrId; + while (($i>1) && ($SrBDef->SrBeg<$BDef->SrBDefOrdered[$SrId-1]->SrBeg)) { + $BDef->SrBDefOrdered[$i] = $BDef->SrBDefOrdered[$i-1]; + $BDef->SrBDefOrdered[$i-1] = $SrBDef; + $i--; + } + // Search next subsection + $SrId++; + $SrName = $BlockName.'_'.$SrId; + $x = false; + $SrLoc = $this->meth_Locator_FindBlockNext($SrSrc,$SrName,0,'.',2,$x,$x); + } while ($SrLoc!==false); + $BDef->SrBDefNbr = $SrId-1; + $BDef->IsSerial = true; + $i = ++$LocR->SectionNbr; + $LocR->SectionLst[$i] = $BDef; + } + } elseif (isset($Loc->PrmLst['parallel'])) { + $BlockLst = $this->meth_Locator_FindParallel($Txt, $Loc->PosBeg, $Loc->PosEnd, $Loc->PrmLst['parallel']); + if ($BlockLst) { + // Store BDefs + foreach ($BlockLst as $i => $Blk) { + if ($Blk['IsRef']) { + $PrBDef = $BDef; + } else { + $PrBDef = $this->meth_Locator_SectionNewBDef($LocR,$BlockName,$Blk['Src'],array(),true); + } + $PrBDef->PosBeg = $Blk['PosBeg']; + $PrBDef->PosEnd = $Blk['PosEnd']; + $i = ++$LocR->SectionNbr; + $LocR->SectionLst[$i] = $PrBDef; + } + $LocR->PosBeg = $BlockLst[0]['PosBeg']; + $LocR->PosEnd = $BlockLst[$LocR->SectionNbr-1]['PosEnd']; + } + } elseif ($BoundPrm !== false) { + $BDef->BoundExpr = $this->meth_Locator_MakeBDefFromField($LocR,$BlockName,$Loc->PrmLst[$BoundPrm],$BoundPrm); + $BDef->ValCurr = null; + $BDef->ValNext = null; + $BDef->CheckPrev = (($BoundChk & 1) != 0); // bitwise check + if ($BDef->CheckPrev) { + $LocR->CheckPrev = true; + $LocR->ValPrev = null; + } + $BDef->CheckNext = (($BoundChk & 2) != 0); // bitwise check + if ($BDef->CheckNext) { + $LocR->CheckNext = true; + $LocR->ValNext = null; + } + if (!$LocR->BoundFound) { + $LocR->BoundFound = true; + $LocR->BoundLst = array(); + $LocR->BoundNb = 0; + $LocR->BoundSingleNb = 0; + } + if ($BoundChk == 3) { + // Insert the singleingrp before all other types + array_splice($LocR->BoundLst, $LocR->BoundSingleNb, 0, array($BDef)); + $LocR->BoundSingleNb++; + } else { + // Insert other types at the end + $LocR->BoundLst[] = $BDef; + } + $LocR->BoundNb++; + } else { + // Normal section + $i = ++$LocR->SectionNbr; + $LocR->SectionLst[$i] = $BDef; + } + + } + + } while ($Loc!==false); + + if ($LocR->WhenFound && ($LocR->SectionNbr===0)) { + // Add a blank section if When is used without a normal section + $BDef = $this->meth_Locator_SectionNewBDef($LocR,$BlockName,'',array(),false); + $LocR->SectionNbr = 1; + $LocR->SectionLst[1] = &$BDef; + } + + return $LocR; // methods return by ref by default + +} + +function meth_Locator_FindParallel(&$Txt, $ZoneBeg, $ZoneEnd, $ConfId) { + + // Define configurations + global $_TBS_ParallelLst; + + if ( ($ConfId=='tbs:table') && (!isset($_TBS_ParallelLst['tbs:table'])) ) { + $_TBS_ParallelLst['tbs:table'] = array( + 'parent' => 'table', + 'ignore' => array('!--', 'caption', 'thead', 'tbody', 'tfoot'), + 'cols' => array(), + 'rows' => array('tr', 'colgroup'), + 'cells' => array('td'=>'colspan', 'th'=>'colspan', 'col'=>'span'), + ); + } + + if (!isset($_TBS_ParallelLst[$ConfId])) return $this->meth_Misc_Alert("Parallel", "The configuration '$ConfId' is not found."); + + $conf = $_TBS_ParallelLst[$ConfId]; + + $Parent = $conf['parent']; + + // Search parent bounds + $par_o = self::f_Xml_FindTag($Txt,$Parent,true ,$ZoneBeg,false,1,false); + if ($par_o===false) return $this->meth_Misc_Alert("Parallel", "The opening tag '$Parent' is not found."); + + $par_c = self::f_Xml_FindTag($Txt,$Parent,false,$ZoneBeg,true,-1,false); + if ($par_c===false) return $this->meth_Misc_Alert("Parallel", "The closing tag '$Parent' is not found."); + + $SrcPOffset = $par_o->PosEnd + 1; + $SrcP = substr($Txt, $SrcPOffset, $par_c->PosBeg - $SrcPOffset); + + // temporary variables + $tagR = ''; + $tagC = ''; + $z = ''; + $pRO = false; + $pROe = false; + $pCO = false; + $pCOe = false; + $p = false; + $Loc = new clsTbsLocator; + + $Rows = array(); + $RowIdx = 0; + $RefRow = false; + $RefCellB= false; + $RefCellE = false; + + $RowType = array(); + + // Loop on entities inside the parent entity + $PosR = 0; + + $mode_column = true; + $Cells = array(); + $ColNum = 1; + $IsRef = false; + + // Search for the next Row Opening tag + while (self::f_Xml_GetNextEntityName($SrcP, $PosR, $tagR, $pRO, $p)) { + + $pROe = strpos($SrcP, '>', $p) + 1; + $singleR = ($SrcP[$pROe-2] === '/'); + + // If the tag is not a closing, a self-closing and has a name + if ($tagR!=='') { + + if (in_array($tagR, $conf['ignore'])) { + // This tag must be ignored + $PosR = $p; + } elseif (isset($conf['cols'][$tagR])) { + // Column definition that must be merged as a cell + if ($mode_column === false) return $this->meth_Misc_Alert("Parallel", "There is a column definition ($tagR) after a row (".$Rows[$RowIdx-1]['tag'].")."); + if (isset($RowType['_column'])) { + $RowType['_column']++; + } else { + $RowType['_column'] = 1; + } + $att = $conf['cols'][$tagR]; + $this->meth_Locator_FindParallelCol($SrcP, $PosR, $tagR, $pRO, $p, $SrcPOffset, $RowIdx, $ZoneBeg, $ZoneEnd, $att, $Loc, $Cells, $ColNum, $IsRef, $RefCellB, $RefCellE, $RefRow); + + } elseif (!$singleR) { + + // Search the Row Closing tag + $locRE = self::f_Xml_FindTag($SrcP, $tagR, false, $pROe, true, -1, false); + if ($locRE===false) return $this->meth_Misc_Alert("Parallel", "The row closing tag is not found. (tagR=$tagR, p=$p, pROe=$pROe)"); + + // Inner source + $SrcR = substr($SrcP, $pROe, $locRE->PosBeg - $pROe); + $SrcROffset = $SrcPOffset + $pROe; + + if (in_array($tagR, $conf['rows'])) { + + if ( $mode_column && isset($RowType['_column']) ) { + $Rows[$RowIdx] = array('tag'=>'_column', 'cells' => $Cells, 'isref' => $IsRef, 'count' => $RowType['_column']); + $RowIdx++; + } + + $mode_column = false; + + if (isset($RowType[$tagR])) { + $RowType[$tagR]++; + } else { + $RowType[$tagR] = 1; + } + + // Now we've got the row entity, we search for cell entities + $Cells = array(); + $ColNum = 1; + $PosC = 0; + $IsRef = false; + + // Loop on Cell Opening tags + while (self::f_Xml_GetNextEntityName($SrcR, $PosC, $tagC, $pCO, $p)) { + if (isset($conf['cells'][$tagC]) ) { + $att = $conf['cells'][$tagC]; + $this->meth_Locator_FindParallelCol($SrcR, $PosC, $tagC, $pCO, $p, $SrcROffset, $RowIdx, $ZoneBeg, $ZoneEnd, $att, $Loc, $Cells, $ColNum, $IsRef, $RefCellB, $RefCellE, $RefRow); + } else { + $PosC = $p; + } + } + + $Rows[$RowIdx] = array('tag'=>$tagR, 'cells' => $Cells, 'isref' => $IsRef, 'count' => $RowType[$tagR]); + $RowIdx++; + + } + + $PosR = $locRE->PosEnd; + + } else { + $PosR = $pROe; + } + } else { + $PosR = $pROe; + } + } + + //return $Rows; + + $Blocks = array(); + $rMax = count($Rows) -1; + foreach ($Rows as $r=>$Row) { + $Cells = $Row['cells']; + if (isset($Cells[$RefCellB]) && $Cells[$RefCellB]['IsBegin']) { + if ( isset($Cells[$RefCellE]) && $Cells[$RefCellE]['IsEnd'] ) { + $PosBeg = $Cells[$RefCellB]['PosBeg']; + $PosEnd = $Cells[$RefCellE]['PosEnd']; + $Blocks[$r] = array( + 'PosBeg' => $PosBeg, + 'PosEnd' => $PosEnd, + 'IsRef' => $Row['isref'], + 'Src' => substr($Txt, $PosBeg, $PosEnd - $PosBeg + 1), + ); + } else { + return $this->meth_Misc_Alert("Parallel", "At row ".$Row['count']." having entity [".$Row['tag']."], the column $RefCellE is missing or is not the last in a set of spanned columns. (The block is defined from column $RefCellB to $RefCellE)"); + } + } else { + return $this->meth_Misc_Alert("Parallel", "At row ".$Row['count']." having entity [".$Row['tag']."],the column $RefCellB is missing or is not the first in a set of spanned columns. (The block is defined from column $RefCellB to $RefCellE)"); + } + } + + return $Blocks; + +} + +function meth_Locator_FindParallelCol($SrcR, &$PosC, $tagC, $pCO, $p, $SrcROffset, $RowIdx, $ZoneBeg, $ZoneEnd, &$att, &$Loc, &$Cells, &$ColNum, &$IsRef, &$RefCellB, &$RefCellE, &$RefRow) { + + $pCOe = false; + + // Read parameters + $Loc->PrmLst = array(); + self::f_Loc_PrmRead($SrcR,$p,true,'\'"','<','>',$Loc,$pCOe,true); + + $singleC = ($SrcR[$pCOe-1] === '/'); + if ($singleC) { + $pCEe = $pCOe; + } else { + // Find the Cell Closing tag + $locCE = self::f_Xml_FindTag($SrcR, $tagC, false, $pCOe, true, -1, false); + if ($locCE===false) return $this->meth_Misc_Alert("Parallel", "The cell closing tag is not found. (pCOe=$pCOe)"); + $pCEe = $locCE->PosEnd; + } + + // Check the cell of reference + $Width = (isset($Loc->PrmLst[$att])) ? intval($Loc->PrmLst[$att]) : 1; + $ColNumE = $ColNum + $Width -1; // Ending Cell + $PosBeg = $SrcROffset + $pCO; + $PosEnd = $SrcROffset + $pCEe; + $OnZone = false; + if ( ($PosBeg <= $ZoneBeg) && ($ZoneBeg <= $PosEnd) && ($RefRow===false) ) { + $RefRow = $RowIdx; + $RefCellB = $ColNum; + $OnZone = true; + $IsRef = true; + } + if ( ($PosBeg <= $ZoneEnd) && ($ZoneEnd <= $PosEnd) ) { + $RefCellE = $ColNum; + $OnZone = true; + } + + // Save info + $Cell = array( + //'_tagR' => $tagR, '_tagC' => $tagC, '_att' => $att, '_OnZone' => $OnZone, '_PrmLst' => $Loc->PrmLst, '_Offset' => $SrcROffset, '_Src' => substr($SrcR, $pCO, $locCE->PosEnd - $pCO + 1), + 'PosBeg' => $PosBeg, + 'PosEnd' => $PosEnd, + 'ColNum' => $ColNum, + 'Width' => $Width, + 'IsBegin' => true, + 'IsEnd' => false, + ); + $Cells[$ColNum] = $Cell; + + // add a virtual column to say if its a ending + if (!isset($Cells[$ColNumE])) $Cells[$ColNumE] = array('IsBegin' => false); + + $Cells[$ColNumE]['IsEnd'] = true; + $Cells[$ColNumE]['PosEnd'] = $Cells[$ColNum]['PosEnd']; + + $PosC = $pCEe; + $ColNum += $Width; + +} + +function meth_Merge_Block(&$Txt,$BlockLst,&$SrcId,&$Query,$SpePrm,$SpeRecNum,$QryPrms=false) { + + $BlockSave = $this->_CurrBlock; + $this->_CurrBlock = $BlockLst; + + // Get source type and info + $Src = new clsTbsDataSource; + if (!$Src->DataPrepare($SrcId,$this)) { + $this->_CurrBlock = $BlockSave; + return 0; + } + + if (is_string($BlockLst)) $BlockLst = explode(',', $BlockLst); + $BlockNbr = count($BlockLst); + $BlockId = 0; + $WasP1 = false; + $NbrRecTot = 0; + $QueryZ = &$Query; + $ReturnData = false; + $Nothing = true; + + while ($BlockId<$BlockNbr) { + + $RecSpe = 0; // Row with a special block's definition (used for the navigation bar) + $QueryOk = true; + $this->_CurrBlock = trim($BlockLst[$BlockId]); + if ($this->_CurrBlock==='*') { + $ReturnData = true; + if ($Src->RecSaved===false) $Src->RecSaving = true; + $this->_CurrBlock = ''; + } + + // Search the block + $LocR = $this->meth_Locator_FindBlockLst($Txt,$this->_CurrBlock,0,$SpePrm); + + if ($LocR->BlockFound) { + + $Nothing = false; + + if ($LocR->Special!==false) $RecSpe = $SpeRecNum; + // OnData + if ($Src->OnDataPrm = isset($LocR->PrmLst['ondata'])) { + $Src->OnDataPrmRef = $LocR->PrmLst['ondata']; + if (isset($Src->OnDataPrmDone[$Src->OnDataPrmRef])) { + $Src->OnDataPrm = false; + } else { + $ErrMsg = false; + if ($this->meth_Misc_UserFctCheck($Src->OnDataPrmRef,'f',$ErrMsg,$ErrMsg,true)) { + $Src->OnDataOk = true; + } else { + $LocR->FullName = $this->_CurrBlock; + $Src->OnDataPrm = $this->meth_Misc_Alert($LocR,'(parameter ondata) '.$ErrMsg,false,'block'); + } + } + } + // Dynamic query + if ($LocR->P1) { + if ( ($LocR->PrmLst['p1']===true) && ((!is_string($Query)) || (strpos($Query,'%p1%')===false)) ) { // p1 with no value is a trick to perform new block with same name + if ($Src->RecSaved===false) $Src->RecSaving = true; + } elseif (is_string($Query)) { + $Src->RecSaved = false; + unset($QueryZ); $QueryZ = ''.$Query; + $i = 1; + do { + $x = 'p'.$i; + if (isset($LocR->PrmLst[$x])) { + $QueryZ = str_replace('%p'.$i.'%',$LocR->PrmLst[$x],$QueryZ); + $i++; + } else { + $i = false; + } + } while ($i!==false); + } + $WasP1 = true; + } elseif (($Src->RecSaved===false) && ($BlockNbr-$BlockId>1)) { + $Src->RecSaving = true; + } + } elseif ($WasP1) { + $QueryOk = false; + $WasP1 = false; + } + + // Open the recordset + if ($QueryOk) { + if ((!$LocR->BlockFound) && (!$LocR->FieldOutside)) { + // Special case: return data without any block to merge + $QueryOk = false; + if ($ReturnData && (!$Src->RecSaved)) { + if ($Src->DataOpen($QueryZ,$QryPrms)) { + do {$Src->DataFetch();} while ($Src->CurrRec!==false); + $Src->DataClose(); + } + } + } else { + $QueryOk = $Src->DataOpen($QueryZ,$QryPrms); + if (!$QueryOk) { + if ($WasP1) { $WasP1 = false;} else {$LocR->FieldOutside = false;} // prevent from infinit loop + } + } + } + + // Merge sections + if ($QueryOk) { + if ($Src->Type===2) { // Special for Text merge + if ($LocR->BlockFound) { + $Txt = substr_replace($Txt,$Src->RecSet,$LocR->PosBeg,$LocR->PosEnd-$LocR->PosBeg+1); + $Src->DataFetch(); // store data, may be needed for multiple blocks + $Src->RecNum = 1; + $Src->CurrRec = false; + } else { + $Src->DataAlert('can\'t merge the block with a text value because the block definition is not found.'); + } + } elseif ($LocR->BlockFound===false) { + $Src->DataFetch(); // Merge first record only + } elseif (isset($LocR->PrmLst['parallel'])) { + $this->meth_Merge_BlockParallel($Txt,$LocR,$Src); + } else { + $this->meth_Merge_BlockSections($Txt,$LocR,$Src,$RecSpe); + } + $Src->DataClose(); // Close the resource + } + + if (!$WasP1) { + $NbrRecTot += $Src->RecNum; + $BlockId++; + } + if ($LocR->FieldOutside) { + $Nothing = false; + $this->meth_Merge_FieldOutside($Txt,$Src->CurrRec,$Src->RecNum,$LocR->FOStop); + } + + } + + // End of the merge + unset($LocR); + $this->_CurrBlock = $BlockSave; + if ($ReturnData) { + return $Src->RecSet; + } else { + unset($Src); + return ($Nothing) ? false : $NbrRecTot; + } + +} + +function meth_Merge_BlockParallel(&$Txt,&$LocR,&$Src) { + + // Main loop + $Src->DataFetch(); + + // Prepare sources + $BlockRes = array(); + for ($i=1 ; $i<=$LocR->SectionNbr ; $i++) { + if ($i>1) { + // Add txt source between the BDefs + $BlockRes[$i] = substr($Txt, $LocR->SectionLst[$i-1]->PosEnd + 1, $LocR->SectionLst[$i]->PosBeg - $LocR->SectionLst[$i-1]->PosEnd -1); + } else { + $BlockRes[$i] = ''; + } + } + + while($Src->CurrRec!==false) { + // Merge the current record with all sections + for ($i=1 ; $i<=$LocR->SectionNbr ; $i++) { + $SecDef = &$LocR->SectionLst[$i]; + $SecSrc = $this->meth_Merge_SectionNormal($SecDef,$Src); + $BlockRes[$i] .= $SecSrc; + } + // Next row + $Src->DataFetch(); + } + + $BlockRes = implode('', $BlockRes); + $Txt = substr_replace($Txt,$BlockRes,$LocR->PosBeg,$LocR->PosEnd-$LocR->PosBeg+1); + +} + +function meth_Merge_BlockSections(&$Txt,&$LocR,&$Src,&$RecSpe) { + + // Initialise + $SecId = 0; + $SecOk = ($LocR->SectionNbr>0); + $SecSrc = ''; + $BlockRes = ''; // The result of the chained merged blocks + $IsSerial = false; + $SrId = 0; + $SrNbr = 0; + $GrpFound = false; + if ($LocR->HeaderFound || $LocR->FooterFound) { + $GrpFound = true; + $piOMG = false; + if ($LocR->FooterFound) { + $Src->PrevSave = true; // $Src->PrevRec will be saved then + } + } + if ($LocR->CheckPrev) $Src->PrevSave = true; + if ($LocR->CheckNext) $Src->NextSave = true; + // Plug-ins + $piOMS = false; + if ($this->_PlugIns_Ok) { + if (isset($this->_piBeforeMergeBlock)) { + $ArgLst = array(&$Txt,&$LocR->PosBeg,&$LocR->PosEnd,$LocR->PrmLst,&$Src,&$LocR); + $this->meth_Plugin_RunAll($this->_piBeforeMergeBlock,$ArgLst); + } + if (isset($this->_piOnMergeSection)) { + $ArgLst = array(&$BlockRes,&$SecSrc); + $piOMS = true; + } + if ($GrpFound && isset($this->_piOnMergeGroup)) { + $ArgLst2 = array(0,0,&$Src,&$LocR); + $piOMG = true; + } + } + + // Main loop + $Src->DataFetch(); + + while($Src->CurrRec!==false) { + + // Headers and Footers + if ($GrpFound) { + $brk_any = false; + $brk_src = ''; // concatenated source to insert as of breaked groups (header and footer) + if ($LocR->FooterFound) { + $brk = false; + for ($i=$LocR->FooterNbr;$i>=1;$i--) { + $GrpDef = &$LocR->FooterDef[$i]; + $x = $this->meth_Merge_SectionNormal($GrpDef->FDef,$Src); // value of the group expression for the current record + if ($Src->RecNum===1) { + // no footer break on first record + $GrpDef->PrevValue = $x; + $brk_i = false; + } else { + // default state for breaked group + if ($GrpDef->AddLastGrp) { + $brk_i = &$brk; // cascading breakings + } else { + unset($brk_i); $brk_i = false; // independent breaking + } + if (!$brk_i) $brk_i = !($GrpDef->PrevValue===$x); + if ($brk_i) { + $brk_any = true; + $ok = true; + if ($piOMG) {$ArgLst2[0]=&$Src->PrevRec; $ArgLst2[1]=&$GrpDef; $ok = $this->meth_PlugIn_RunAll($this->_piOnMergeGroup,$ArgLst2);} + if ($ok!==false) $brk_src = $this->meth_Merge_SectionNormal($GrpDef,$Src->PrevRec).$brk_src; + $GrpDef->PrevValue = $x; + } + } + } + } + if ($LocR->HeaderFound) { + // Check if the current record breaks any header group + $brk = ($Src->RecNum===1); // there is always a header break on first record + for ($i=1;$i<=$LocR->HeaderNbr;$i++) { + $GrpDef = &$LocR->HeaderDef[$i]; + $x = $this->meth_Merge_SectionNormal($GrpDef->FDef,$Src); // value of the group expression for the current record + if (!$brk) $brk = !($GrpDef->PrevValue===$x); // cascading breakings + if ($brk) { + $ok = true; + if ($piOMG) {$ArgLst2[0]=&$Src; $ArgLst2[1]=&$GrpDef; $ok = $this->meth_PlugIn_RunAll($this->_piOnMergeGroup,$ArgLst2);} + if ($ok!==false) $brk_src .= $this->meth_Merge_SectionNormal($GrpDef,$Src); + $GrpDef->PrevValue = $x; + } + } + $brk_any = ($brk_any || $brk); + } + if ($brk_any) { + if ($IsSerial) { + $BlockRes .= $this->meth_Merge_SectionSerial($SecDef,$SrId,$LocR); + $IsSerial = false; + } + $BlockRes .= $brk_src; + } + } // end of header and footer + + // Increment Section + if (($IsSerial===false) && $SecOk) { + $SecId++; + if ($SecId>$LocR->SectionNbr) $SecId = 1; + $SecDef = &$LocR->SectionLst[$SecId]; + $IsSerial = $SecDef->IsSerial; + if ($IsSerial) { + $SrId = 0; + $SrNbr = $SecDef->SrBDefNbr; + } + } + + // Serial Mode Activation + if ($IsSerial) { // Serial Merge + $SrId++; + $SrBDef = &$SecDef->SrBDefLst[$SrId]; + $SrBDef->SrTxt = $this->meth_Merge_SectionNormal($SrBDef,$Src); + if ($SrId>=$SrNbr) { + $SecSrc = $this->meth_Merge_SectionSerial($SecDef,$SrId,$LocR); + $BlockRes .= $SecSrc; + $IsSerial = false; + } + } else { // Classic merge + if ($SecOk) { + // There is some normal sections + if ($Src->RecNum===$RecSpe) { + $SecDef = &$LocR->Special; + } elseif ($LocR->BoundFound) { + $first = true; + for ($i = 0 ; $i < $LocR->BoundNb ; $i++) { + // all bounds must be tested in order to update next and prev values, but only the first found must be kept + if ($this->meth_Merge_CheckBounds($LocR->BoundLst[$i],$Src)) { + if ($first) $SecDef = &$LocR->BoundLst[$i]; + $first = false; + } + } + } + $SecSrc = $this->meth_Merge_SectionNormal($SecDef,$Src); + } else { + // No normal section + $SecSrc = ''; + } + // Conditional blocks + if ($LocR->WhenFound) { + $found = false; + $continue = true; + $i = 1; + do { + $WhenBDef = &$LocR->WhenLst[$i]; + $cond = $this->meth_Merge_SectionNormal($WhenBDef->WhenCond,$Src); // conditional expression for the current record + if ($this->f_Misc_CheckCondition($cond)) { + $x_when = $this->meth_Merge_SectionNormal($WhenBDef,$Src); + $SecSrc = ($WhenBDef->WhenBeforeNS) ? $x_when.$SecSrc : $SecSrc.$x_when; + $found = true; + if ($LocR->WhenSeveral===false) $continue = false; + } + $i++; + if ($i>$LocR->WhenNbr) $continue = false; + } while ($continue); + if (($found===false) && ($LocR->WhenDefault!==false)) { + $x_when = $this->meth_Merge_SectionNormal($LocR->WhenDefault,$Src); + $SecSrc = ($LocR->WhenDefaultBeforeNS) ? $x_when.$SecSrc : $SecSrc.$x_when; + } + } + if ($piOMS) $this->meth_PlugIn_RunAll($this->_piOnMergeSection,$ArgLst); + $BlockRes .= $SecSrc; + } + + // Next record + $Src->DataFetch(); + + } //--> while($CurrRec!==false) { + + // At this point, all data has been fetched. + + // Source to add after the last record + $SecSrc = ''; + + // Serial: merge the extra the sub-blocks + if ($IsSerial) $SecSrc .= $this->meth_Merge_SectionSerial($SecDef,$SrId,$LocR); + + // Add all footers after the last record + if ($LocR->FooterFound) { + if ($Src->RecNum>0) { + for ($i=1;$i<=$LocR->FooterNbr;$i++) { + $GrpDef = &$LocR->FooterDef[$i]; + if ($GrpDef->AddLastGrp) { + $ok = true; + if ($piOMG) {$ArgLst2[0]=&$Src->PrevRec; $ArgLst2[1]=&$GrpDef; $ok = $this->meth_PlugIn_RunAll($this->_piOnMergeGroup,$ArgLst2);} + if ($ok!==false) $SecSrc .= $this->meth_Merge_SectionNormal($GrpDef,$Src->PrevRec); + } + } + } + } + + // NoData + if ($Src->RecNum===0) { + if ($LocR->NoData!==false) { + $SecSrc = $LocR->NoData->Src; + } elseif(isset($LocR->PrmLst['bmagnet'])) { + $this->f_Loc_EnlargeToTag($Txt,$LocR,$LocR->PrmLst['bmagnet'],false); + } + } + + // Plug-ins + if ($piOMS && ($SecSrc!=='')) $this->meth_PlugIn_RunAll($this->_piOnMergeSection,$ArgLst); + + $BlockRes .= $SecSrc; + + // Plug-ins + if ($this->_PlugIns_Ok && isset($ArgLst) && isset($this->_piAfterMergeBlock)) { + $ArgLst = array(&$BlockRes,&$Src,&$LocR); + $this->meth_PlugIn_RunAll($this->_piAfterMergeBlock,$ArgLst); + } + + // Merge the result + $Txt = substr_replace($Txt,$BlockRes,$LocR->PosBeg,$LocR->PosEnd-$LocR->PosBeg+1); + if ($LocR->P1) $LocR->FOStop = $LocR->PosBeg + strlen($BlockRes) -1; + +} + +function meth_Merge_AutoVar(&$Txt,$ConvStr,$Id='var') { +// Merge automatic fields with VarRef + + $Pref = &$this->VarPrefix; + $PrefL = strlen($Pref); + $PrefOk = ($PrefL>0); + + if ($ConvStr===false) { + $Charset = $this->Charset; + $this->Charset = false; + } + + // Then we scann all fields in the model + $x = ''; + $Pos = 0; + while ($Loc = $this->meth_Locator_FindTbs($Txt,$Id,$Pos,'.')) { + if ($Loc->SubNbr==0) $Loc->SubLst[0]=''; // In order to force error message + if ($Loc->SubLst[0]==='') { + $Pos = $this->meth_Merge_AutoSpe($Txt,$Loc); + } elseif ($Loc->SubLst[0][0]==='~') { + if (!isset($ObjOk)) $ObjOk = (is_object($this->ObjectRef) || is_array($this->ObjectRef)); + if ($ObjOk) { + $Loc->SubLst[0] = substr($Loc->SubLst[0],1); + $Pos = $this->meth_Locator_Replace($Txt,$Loc,$this->ObjectRef,0); + } elseif (isset($Loc->PrmLst['noerr'])) { + $Pos = $this->meth_Locator_Replace($Txt,$Loc,$x,false); + } else { + $this->meth_Misc_Alert($Loc,'property ObjectRef is neither an object nor an array. Its type is \''.gettype($this->ObjectRef).'\'.',true); + $Pos = $Loc->PosEnd + 1; + } + } elseif ($PrefOk && (substr($Loc->SubLst[0],0,$PrefL)!==$Pref)) { + if (isset($Loc->PrmLst['noerr'])) { + $Pos = $this->meth_Locator_Replace($Txt,$Loc,$x,false); + } else { + $this->meth_Misc_Alert($Loc,'does not match the allowed prefix.',true); + $Pos = $Loc->PosEnd + 1; + } + } elseif ( isset($this->VarRef) && isset($this->VarRef[$Loc->SubLst[0]])) { + $Pos = $this->meth_Locator_Replace($Txt,$Loc, $this->VarRef[$Loc->SubLst[0]], 1); + } elseif ( is_null($this->VarRef) && isset($GLOBALS[$Loc->SubLst[0]]) ) { + $Pos = $this->meth_Locator_Replace($Txt,$Loc, $GLOBALS[$Loc->SubLst[0]], 1); + } else { + if (isset($Loc->PrmLst['noerr'])) { + $Pos = $this->meth_Locator_Replace($Txt,$Loc,$x,false); + } else { + $Pos = $Loc->PosEnd + 1; + $msg = (is_null($this->VarRef)) ? 'VarRef seems refers to $GLOBALS' : 'VarRef seems refers to a custom array of values'; + $this->meth_Misc_Alert($Loc,'the key \''.$Loc->SubLst[0].'\' does not exist or is not set in VarRef. ('.$msg.')',true); + } + } + } + + if ($ConvStr===false) $this->Charset = $Charset; + + return false; // Useful for properties PrmIfVar & PrmThenVar + +} + +function meth_Merge_AutoSpe(&$Txt,&$Loc) { +// Merge Special Var Fields ([var..*]) + + $ErrMsg = false; + $SubStart = false; + if (isset($Loc->SubLst[1])) { + switch ($Loc->SubLst[1]) { + case 'now': $x = time(); break; + case 'version': $x = $this->Version; break; + case 'script_name': $x = basename(((isset($_SERVER)) ? $_SERVER['PHP_SELF'] : $GLOBALS['HTTP_SERVER_VARS']['PHP_SELF'] )); break; + case 'template_name': $x = $this->_LastFile; break; + case 'template_date': $x = ''; if ($this->f_Misc_GetFile($x,$this->_LastFile,'',array(),false)) $x = $x['mtime']; break; + case 'template_path': $x = dirname($this->_LastFile).'/'; break; + case 'name': $x = 'TinyButStrong'; break; + case 'logo': $x = '**TinyButStrong**'; break; + case 'charset': $x = $this->Charset; break; + case 'error_msg': $this->_ErrMsgName = $Loc->FullName; return $Loc->PosEnd; break; + case '': $ErrMsg = 'it doesn\'t have any keyword.'; break; + case 'tplvars': + if ($Loc->SubNbr==2) { + $SubStart = 2; + $x = implode(',',array_keys($this->TplVars)); // list of all template variables + } else { + if (isset($this->TplVars[$Loc->SubLst[2]])) { + $SubStart = 3; + $x = &$this->TplVars[$Loc->SubLst[2]]; + } else { + $ErrMsg = 'property TplVars doesn\'t have any item named \''.$Loc->SubLst[2].'\'.'; + } + } + break; + case 'store': + if ($Loc->SubNbr==2) { + $SubStart = 2; + $x = implode('',$this->TplStore); // concatenation of all stores + } else { + if (isset($this->TplStore[$Loc->SubLst[2]])) { + $SubStart = 3; + $x = &$this->TplStore[$Loc->SubLst[2]]; + } else { + $ErrMsg = 'Store named \''.$Loc->SubLst[2].'\' is not defined yet.'; + } + } + if (!isset($Loc->PrmLst['strconv'])) {$Loc->PrmLst['strconv'] = 'no'; $Loc->PrmLst['protect'] = 'no';} + break; + case 'cst': $x = @constant($Loc->SubLst[2]); break; + case 'tbs_info': + $x = 'TinyButStrong version '.$this->Version.' for PHP 5'; + $x .= "\r\nInstalled plug-ins: ".count($this->_PlugIns); + foreach (array_keys($this->_PlugIns) as $pi) { + $o = &$this->_PlugIns[$pi]; + $x .= "\r\n- plug-in [".(isset($o->Name) ? $o->Name : $pi ).'] version '.(isset($o->Version) ? $o->Version : '?' ); + } + break; + case 'php_info': + ob_start(); + phpinfo(); + $x = ob_get_contents(); + ob_end_clean(); + $x = self::f_Xml_GetPart($x, '(style)+body', false); + if (!isset($Loc->PrmLst['strconv'])) {$Loc->PrmLst['strconv'] = 'no'; $Loc->PrmLst['protect'] = 'no';} + break; + default: + $IsSupported = false; + if (isset($this->_piOnSpecialVar)) { + $x = ''; + $ArgLst = array(substr($Loc->SubName,1),&$IsSupported ,&$x, &$Loc->PrmLst,&$Txt,&$Loc->PosBeg,&$Loc->PosEnd,&$Loc); + $this->meth_PlugIn_RunAll($this->_piOnSpecialVar,$ArgLst); + } + if (!$IsSupported) $ErrMsg = '\''.$Loc->SubLst[1].'\' is an unsupported keyword.'; + } + } else { + $ErrMsg = 'it doesn\'t have any subname.'; + } + if ($ErrMsg!==false) { + $this->meth_Misc_Alert($Loc,$ErrMsg); + $x = ''; + } + if ($Loc->PosBeg===false) { + return $Loc->PosEnd; + } else { + return $this->meth_Locator_Replace($Txt,$Loc,$x,$SubStart); + } +} + +function meth_Merge_FieldOutside(&$Txt, &$CurrRec, $RecNum, $PosMax) { + $Pos = 0; + $SubStart = ($CurrRec===false) ? false : 0; + do { + $Loc = $this->meth_Locator_FindTbs($Txt,$this->_CurrBlock,$Pos,'.'); + if ($Loc!==false) { + if (($PosMax!==false) && ($Loc->PosEnd>$PosMax)) return; + if ($Loc->SubName==='#') { + $NewEnd = $this->meth_Locator_Replace($Txt,$Loc,$RecNum,false); + } else { + $NewEnd = $this->meth_Locator_Replace($Txt,$Loc,$CurrRec,$SubStart); + } + if ($PosMax!==false) $PosMax += $NewEnd - $Loc->PosEnd; + $Pos = $NewEnd; + } + } while ($Loc!==false); +} + +/** + * Check the values of previous and next record for expression. + * + * @return boolean + */ +function meth_Merge_CheckBounds($BDef,$Src) { + + // Retrieve values considering that a new record is fetched + // The order is important + if ($BDef->CheckPrev) { + $BDef->ValPrev = $BDef->ValCurr; + } + if ($BDef->CheckNext) { + if (is_null($BDef->ValNext)) { + // ValNext is not set at this point for the very first record + $BDef->ValCurr = $this->meth_Merge_SectionNormal($BDef->BoundExpr,$Src); + } else { + $BDef->ValCurr = $BDef->ValNext; + } + if ($Src->NextRec->CurrRec === false) { + // No next record + $diff_next = true; + } else { + $BDef->ValNext = $this->meth_Merge_SectionNormal($BDef->BoundExpr,$Src->NextRec); // merge with next record + $diff_next = ($BDef->ValCurr !== $BDef->ValNext); + } + } else { + $BDef->ValCurr = $this->meth_Merge_SectionNormal($BDef->BoundExpr,$Src); // merge with current record + } + + // Check values + $result = false; // this state must never happen + if ($BDef->CheckPrev) { + $diff_prev = ($BDef->ValCurr !== $BDef->ValPrev); + if ($BDef->CheckNext) { + $result = $diff_prev && $diff_next; + } else { + $result = $diff_prev; + } + } elseif ($BDef->CheckNext) { + $result = $diff_next; + } + + return $result; + +} + +function meth_Merge_SectionNormal(&$BDef,&$Src) { + + $Txt = $BDef->Src; + $LocLst = &$BDef->LocLst; + $iMax = $BDef->LocNbr; + $PosMax = strlen($Txt); + + if ($Src===false) { // Erase all fields + + $x = ''; + + // Chached locators + for ($i=$iMax;$i>0;$i--) { + if ($LocLst[$i]->PosBeg<$PosMax) { + $this->meth_Locator_Replace($Txt,$LocLst[$i],$x,false); + if ($LocLst[$i]->Enlarged) { + $PosMax = $LocLst[$i]->PosBeg; + $LocLst[$i]->PosBeg = $LocLst[$i]->PosBeg0; + $LocLst[$i]->PosEnd = $LocLst[$i]->PosEnd0; + $LocLst[$i]->Enlarged = false; + } + } + } + + // Uncached locators + if ($BDef->Chk) { + $BlockName = &$BDef->Name; + $Pos = 0; + while ($Loc = $this->meth_Locator_FindTbs($Txt,$BlockName,$Pos,'.')) $Pos = $this->meth_Locator_Replace($Txt,$Loc,$x,false); + } + + } else { + + // Cached locators + for ($i=$iMax;$i>0;$i--) { + if ($LocLst[$i]->PosBeg<$PosMax) { + if ($LocLst[$i]->IsRecInfo) { + if ($LocLst[$i]->RecInfo==='#') { + $this->meth_Locator_Replace($Txt,$LocLst[$i],$Src->RecNum,false); + } else { + $this->meth_Locator_Replace($Txt,$LocLst[$i],$Src->RecKey,false); + } + } else { + $this->meth_Locator_Replace($Txt,$LocLst[$i],$Src->CurrRec,0); + } + if ($LocLst[$i]->Enlarged) { + $PosMax = $LocLst[$i]->PosBeg; + $LocLst[$i]->PosBeg = $LocLst[$i]->PosBeg0; + $LocLst[$i]->PosEnd = $LocLst[$i]->PosEnd0; + $LocLst[$i]->Enlarged = false; + } + } + } + + // Unchached locators + if ($BDef->Chk) { + $BlockName = &$BDef->Name; + foreach ($Src->CurrRec as $key => $val) { + $Pos = 0; + $Name = $BlockName.'.'.$key; + while ($Loc = $this->meth_Locator_FindTbs($Txt,$Name,$Pos,'.')) $Pos = $this->meth_Locator_Replace($Txt,$Loc,$val,0); + } + $Pos = 0; + $Name = $BlockName.'.#'; + while ($Loc = $this->meth_Locator_FindTbs($Txt,$Name,$Pos,'.')) $Pos = $this->meth_Locator_Replace($Txt,$Loc,$Src->RecNum,0); + $Pos = 0; + $Name = $BlockName.'.$'; + while ($Loc = $this->meth_Locator_FindTbs($Txt,$Name,$Pos,'.')) $Pos = $this->meth_Locator_Replace($Txt,$Loc,$Src->RecKey,0); + } + + } + + // Automatic sub-blocks + if (isset($BDef->AutoSub)) { + for ($i=1;$i<=$BDef->AutoSub;$i++) { + $name = $BDef->Name.'_sub'.$i; + $query = ''; + $col = $BDef->Prm['sub'.$i]; + if ($col===true) $col = ''; + $col_opt = (substr($col,0,1)==='(') && (substr($col,-1,1)===')'); + if ($col_opt) $col = substr($col,1,strlen($col)-2); + if ($col==='') { + // $col_opt cannot be used here because values which are not array nore object are reformated by $Src into an array with keys 'key' and 'val' + $data = &$Src->CurrRec; + } elseif (is_object($Src->CurrRec)) { + $data = &$Src->CurrRec->$col; + } else { + if (array_key_exists($col, $Src->CurrRec)) { + $data = &$Src->CurrRec[$col]; + } else { + if (!$col_opt) $this->meth_Misc_Alert('for merging the automatic sub-block ['.$name.']','key \''.$col.'\' is not found in record #'.$Src->RecNum.' of block ['.$BDef->Name.']. This key can become optional if you designate it with parenthesis in the main block, i.e.: sub'.$i.'=('.$col.')'); + unset($data); $data = array(); + } + } + if (is_string($data)) { + $data = explode(',',$data); + } elseif (is_null($data) || ($data===false)) { + $data = array(); + } + $this->meth_Merge_Block($Txt, $name, $data, $query, false, 0, false); + } + } + + return $Txt; + +} + +function meth_Merge_SectionSerial(&$BDef,&$SrId,&$LocR) { + + $Txt = $BDef->Src; + $SrBDefOrdered = &$BDef->SrBDefOrdered; + $Empty = &$LocR->SerialEmpty; + + // All Items + $F = false; + for ($i=$BDef->SrBDefNbr;$i>0;$i--) { + $SrBDef = &$SrBDefOrdered[$i]; + if ($SrBDef->SrTxt===false) { // Subsection not merged with a record + if ($Empty===false) { + $SrBDef->SrTxt = $this->meth_Merge_SectionNormal($SrBDef,$F); + } else { + $SrBDef->SrTxt = $Empty; + } + } + $Txt = substr_replace($Txt,$SrBDef->SrTxt,$SrBDef->SrBeg,$SrBDef->SrLen); + $SrBDef->SrTxt = false; + } + + $SrId = 0; + return $Txt; + +} + +/** + * Merge [onload] or [onshow] fields and blocks + */ +function meth_Merge_AutoOn(&$Txt,$Name,$TplVar,$MergeVar) { + + $GrpDisplayed = array(); + $GrpExclusive = array(); + $P1 = false; + $FieldBefore = false; + $Pos = 0; + + while ($LocA=$this->meth_Locator_FindBlockNext($Txt,$Name,$Pos,'_',1,$P1,$FieldBefore)) { + + if ($LocA->BlockFound) { + + if (!isset($GrpDisplayed[$LocA->SubName])) { + $GrpDisplayed[$LocA->SubName] = false; + $GrpExclusive[$LocA->SubName] = ($LocA->SubName!==''); + } + $Displayed = &$GrpDisplayed[$LocA->SubName]; + $Exclusive = &$GrpExclusive[$LocA->SubName]; + + $DelBlock = false; + $DelField = false; + if ($Displayed && $Exclusive) { + $DelBlock = true; + } else { + if (isset($LocA->PrmLst['when'])) { + if (isset($LocA->PrmLst['several'])) $Exclusive=false; + $x = $LocA->PrmLst['when']; + $this->meth_Merge_AutoVar($x,false); + if ($this->f_Misc_CheckCondition($x)) { + $DelField = true; + $Displayed = true; + } else { + $DelBlock = true; + } + } elseif(isset($LocA->PrmLst['default'])) { + if ($Displayed) { + $DelBlock = true; + } else { + $Displayed = true; + $DelField = true; + } + $Exclusive = true; // No more block displayed for the group after + } + } + + // Del parts + if ($DelField) { + if ($LocA->PosBeg2!==false) $Txt = substr_replace($Txt, '', $LocA->PosBeg2, $LocA->PosEnd2 - $LocA->PosBeg2 + 1); + $Txt = substr_replace($Txt,'',$LocA->PosBeg,$LocA->PosEnd-$LocA->PosBeg+1); + $Pos = $LocA->PosBeg; + } else { + $FldPos = $LocA->PosBeg; + $FldLen = $LocA->PosEnd - $LocA->PosBeg + 1; + if ($LocA->PosBeg2===false) { + if ($this->f_Loc_EnlargeToTag($Txt,$LocA,$LocA->PrmLst['block'],false)===false) $this->meth_Misc_Alert($LocA,'at least one tag corresponding to '.$LocA->PrmLst['block'].' is not found. Check opening tags, closing tags and embedding levels.',false,'in block\'s definition'); + } else { + $LocA->PosEnd = $LocA->PosEnd2; + } + if ($DelBlock) { + $parallel = false; + if (isset($LocA->PrmLst['parallel'])) { + // may return false if error + $parallel = $this->meth_Locator_FindParallel($Txt, $LocA->PosBeg, $LocA->PosEnd, $LocA->PrmLst['parallel']); + if ($parallel===false) { + $Txt = substr_replace($Txt,'',$FldPos,$FldLen); + } else { + // delete in reverse order + for ($r = count($parallel)-1 ; $r >= 0 ; $r--) { + $p = $parallel[$r]; + $Txt = substr_replace($Txt,'',$p['PosBeg'],$p['PosEnd']-$p['PosBeg']+1); + } + } + } else { + $Txt = substr_replace($Txt,'',$LocA->PosBeg,$LocA->PosEnd-$LocA->PosBeg+1); + } + $Pos = $LocA->PosBeg; + } else { + // Merge the block as if it was a field + $x = ''; + $this->meth_Locator_Replace($Txt,$LocA,$x,false); + $Pos = $LocA->PosNext; + } + } + + } else { // Field (the Loc has no subname at this point) + + // Check for Template Var + if ($TplVar) { + if (isset($LocA->PrmLst['tplvars']) || isset($LocA->PrmLst['tplfrms'])) { + $Scan = ''; + foreach ($LocA->PrmLst as $Key => $Val) { + if ($Scan=='v') { + $this->TplVars[$Key] = $Val; + } elseif ($Scan=='f') { + self::f_Misc_FormatSave($Val,$Key); + } elseif ($Key==='tplvars') { + $Scan = 'v'; + } elseif ($Key==='tplfrms') { + $Scan = 'f'; + } + } + } + } + + $x = ''; + $this->meth_Locator_Replace($Txt,$LocA,$x,false); + $Pos = $LocA->PosNext; // continue at the start so embedded fields can be merged + + } + + } + + if ($MergeVar) $this->meth_Merge_AutoVar($Txt,true,$Name); // merge other fields (must have subnames) + + foreach ($this->Assigned as $n=>$a) { + if (isset($a['auto']) && ($a['auto']===$Name)) { + $x = array(); + $this->meth_Misc_Assign($n,$x,false); + } + } + +} + +// Prepare the strconv parameter +function meth_Conv_Prepare(&$Loc, $StrConv) { + $x = strtolower($StrConv); + $x = '+'.str_replace(' ','',$x).'+'; + if (strpos($x,'+esc+')!==false) {$this->f_Misc_ConvSpe($Loc); $Loc->ConvStr = false; $Loc->ConvEsc = true; } + if (strpos($x,'+wsp+')!==false) {$this->f_Misc_ConvSpe($Loc); $Loc->ConvWS = true; } + if (strpos($x,'+js+')!==false) {$this->f_Misc_ConvSpe($Loc); $Loc->ConvStr = false; $Loc->ConvJS = true; } + if (strpos($x,'+url+')!==false) {$this->f_Misc_ConvSpe($Loc); $Loc->ConvStr = false; $Loc->ConvUrl = true; } + if (strpos($x,'+utf8+')!==false) {$this->f_Misc_ConvSpe($Loc); $Loc->ConvStr = false; $Loc->ConvUtf8 = true; } + if (strpos($x,'+no+')!==false) $Loc->ConvStr = false; + if (strpos($x,'+yes+')!==false) $Loc->ConvStr = true; + if (strpos($x,'+nobr+')!==false) {$Loc->ConvStr = true; $Loc->ConvBr = false; } +} + +// Convert a string with charset or custom function +function meth_Conv_Str(&$Txt,$ConvBr=true) { + if ($this->Charset==='') { // Html by default + $Txt = htmlspecialchars($Txt, ENT_COMPAT); // ENT_COMPAT is no more the default value since PHP 8.1 + if ($ConvBr) $Txt = nl2br($Txt); + } elseif ($this->_CharsetFct) { + $Txt = call_user_func($this->Charset, $Txt,$ConvBr); + } else { + $Txt = htmlspecialchars($Txt, ENT_COMPAT, $this->Charset); + if ($ConvBr) $Txt = nl2br($Txt); + } +} + +// Standard alert message provided by TinyButStrong, return False is the message is cancelled. +function meth_Misc_Alert($Src,$Msg,$NoErrMsg=false,$SrcType=false) { + $this->ErrCount++; + if ($this->NoErr || (PHP_SAPI==='cli') ) { + $t = array('','','','',''); + } else { + $t = array('
','','','','
'); + $Msg = htmlentities($Msg); + } + if (!is_string($Src)) { + if ($SrcType===false) $SrcType='in field'; + if (isset($Src->PrmLst['tbstype'])) { + $Msg = 'Column \''.$Src->SubName.'\' is expected but missing in the current record.'; + $Src = 'Parameter \''.$Src->PrmLst['tbstype'].'='.$Src->SubName.'\''; + $NoErrMsg = false; + } else { + $Src = $SrcType.' '.$this->_ChrOpen.$Src->FullName.'...'.$this->_ChrClose; + } + } + $x = $t[0].'TinyButStrong Error'.$t[1].' '.$Src.': '.$Msg; + if ($NoErrMsg) $x = $x.' '.$t[2].'This message can be cancelled using parameter \'noerr\'.'.$t[3]; + $x = $x.$t[4]."\n"; + if ($this->NoErr) { + $this->ErrMsg .= $x; + } else { + if (PHP_SAPI!=='cli') { + $x = str_replace($this->_ChrOpen,$this->_ChrProtect,$x); + } + echo $x; + } + return false; +} + +function meth_Misc_Assign($Name,&$ArgLst,$CallingMeth) { +// $ArgLst must be by reference in order to have its inner items by reference too. + + if (!isset($this->Assigned[$Name])) { + if ($CallingMeth===false) return true; + return $this->meth_Misc_Alert('with '.$CallingMeth.'() method','key \''.$Name.'\' is not defined in property Assigned.'); + } + + $a = &$this->Assigned[$Name]; + $meth = (isset($a['type'])) ? $a['type'] : 'MergeBlock'; + if (($CallingMeth!==false) && (strcasecmp($CallingMeth,$meth)!=0)) return $this->meth_Misc_Alert('with '.$CallingMeth.'() method','the assigned key \''.$Name.'\' cannot be used with method '.$CallingMeth.' because it is defined to run with '.$meth.'.'); + + $n = count($a); + for ($i=0;$i<$n;$i++) { + if (isset($a[$i])) $ArgLst[$i] = &$a[$i]; + } + + if ($CallingMeth===false) { + if (in_array(strtolower($meth),array('mergeblock','mergefield'))) { + call_user_func_array(array(&$this,$meth), $ArgLst); + } else { + return $this->meth_Misc_Alert('', 'The assigned field \''.$Name.'\'. cannot be merged because its type \''.$a[0].'\' is not supported.'); + } + } + if (!isset($a['merged'])) $a['merged'] = 0; + $a['merged']++; + return true; +} + +function meth_Misc_IsMainTpl() { + return ($this->_Mode==0); +} + +function meth_Misc_ChangeMode($Init,&$Loc,&$CurrVal) { + if ($Init) { + // Save contents configuration + $Loc->SaveSrc = &$this->Source; + $Loc->SaveMode = $this->_Mode; + $Loc->SaveVarRef = &$this->VarRef; + unset($this->Source); $this->Source = ''; + $this->_Mode++; // Mode>0 means subtemplate mode + if ($this->OldSubTpl) { + ob_start(); // Start buffuring output + $Loc->SaveRender = $this->Render; + } + $this->Render = TBS_OUTPUT; + } else { + // Restore contents configuration + if ($this->OldSubTpl) { + $CurrVal = ob_get_contents(); + ob_end_clean(); + $this->Render = $Loc->SaveRender; + } else { + $CurrVal = $this->Source; + } + $this->Source = &$Loc->SaveSrc; + $this->_Mode = $Loc->SaveMode; + $this->VarRef = &$Loc->SaveVarRef; + } +} + +function meth_Misc_UserFctCheck(&$FctInfo,$FctCat,&$FctObj,&$ErrMsg,$FctCheck=false) { + + $FctId = $FctCat.':'.$FctInfo; + if (isset($this->_UserFctLst[$FctId])) { + $FctInfo = $this->_UserFctLst[$FctId]; + return true; + } + + // Check and put in cache + $FctStr = $FctInfo; + $IsData = ($FctCat!=='f'); + $Save = true; + if ($FctStr[0]==='~') { + $ObjRef = &$this->ObjectRef; + $Lst = explode('.',substr($FctStr,1)); + $iMax = count($Lst) - 1; + $Suff = 'tbsdb'; + $iMax0 = $iMax; + if ($IsData) { + $Suff = $Lst[$iMax]; + $iMax--; + } + // Reading sub items + for ($i=0;$i<=$iMax;$i++) { + $x = &$Lst[$i]; + if (is_object($ObjRef)) { + $form = $this->f_Misc_ParseFctForm($x); + $n = $form['name']; + if ($i === $iMax0) { + // last item is supposed to be a function's name, without parenthesis + if ( method_exists($ObjRef,$n) || (method_exists($ObjRef, '__call'))) { + // Ok, continue. If $form['as_fct'] is true, then it will produce an error when try to call function $x + } else { + $ErrMsg = 'Expression \''.$FctStr.'\' is invalid because \''.$n.'\' is not a method in the class \''.get_class($ObjRef).'\'.'; + return false; + } + } elseif ( method_exists($ObjRef,$n) || ($form['as_fct'] && method_exists($ObjRef, 'x__call')) ) { + $f = array(&$ObjRef,$n); + unset($ObjRef); + $ObjRef = call_user_func_array($f,$form['args']); + } elseif (isset($ObjRef->$n)) { + $ObjRef = &$ObjRef->$n; + } else { + $ErrMsg = 'Expression \''.$FctStr.'\' is invalid because sub-item \''.$n.'\' is neither a method nor a property in the class \''.get_class($ObjRef).'\'.'; + return false; + } + } elseif (($i<$iMax0) && is_array($ObjRef)) { + if (isset($ObjRef[$x])) { + $ObjRef = &$ObjRef[$x]; + } else { + $ErrMsg = 'Expression \''.$FctStr.'\' is invalid because sub-item \''.$x.'\' is not a existing key in the array.'; + return false; + } + } else { + $ErrMsg = 'Expression \''.$FctStr.'\' is invalid because '.(($i===0)?'property ObjectRef':'sub-item \''.$x.'\'').' is not an object'.(($i<$iMax)?' or an array.':'.'); + return false; + } + } + // Referencing last item + if ($IsData) { + $FctInfo = array('open'=>'','fetch'=>'','close'=>''); + foreach ($FctInfo as $act=>$x) { + $FctName = $Suff.'_'.$act; + if (method_exists($ObjRef,$FctName)) { + $FctInfo[$act] = array(&$ObjRef,$FctName); + } else { + $ErrMsg = 'Expression \''.$FctStr.'\' is invalid because method '.$FctName.' is not found.'; + return false; + } + } + $FctInfo['type'] = 4; + if (isset($this->RecheckObj) && $this->RecheckObj) $Save = false; + } else { + $FctInfo = array(&$ObjRef,$x); + } + } elseif ($IsData) { + + $IsObj = ($FctCat==='o'); + + if ($IsObj && method_exists($FctObj,'tbsdb_open') && (!method_exists($FctObj,'+'))) { // '+' avoid a bug in PHP 5 + + if (!method_exists($FctObj,'tbsdb_fetch')) { + $ErrMsg = 'the expected method \'tbsdb_fetch\' is not found for the class '.$Cls.'.'; + return false; + } + if (!method_exists($FctObj,'tbsdb_close')) { + $ErrMsg = 'the expected method \'tbsdb_close\' is not found for the class '.$Cls.'.'; + return false; + } + $FctInfo = array('type'=>5); + + } else { + + if ($FctCat==='r') { // Resource + $x = strtolower($FctStr); + $x = str_replace('-','_',$x); + $Key = ''; + $i = 0; + $iMax = strlen($x); + while ($i<$iMax) { + if (($x[$i]==='_') || (($x[$i]>='a') && ($x[$i]<='z')) || (($x[$i]>='0') && ($x[$i]<='9'))) { + $Key .= $x[$i]; + $i++; + } else { + $i = $iMax; + } + } + } else { + $Key = $FctStr; + } + + $FctInfo = array('open'=>'','fetch'=>'','close'=>''); + foreach ($FctInfo as $act=>$x) { + $FctName = 'tbsdb_'.$Key.'_'.$act; + if (function_exists($FctName)) { + $FctInfo[$act] = $FctName; + } else { + $err = true; + if ($act==='open') { // Try simplified key + $p = strpos($Key,'_'); + if ($p!==false) { + $Key2 = substr($Key,0,$p); + $FctName2 = 'tbsdb_'.$Key2.'_'.$act; + if (function_exists($FctName2)) { + $err = false; + $Key = $Key2; + $FctInfo[$act] = $FctName2; + } + } + } + if ($err) { + $ErrMsg = 'Data source Id \''.$FctStr.'\' is unsupported because function \''.$FctName.'\' is not found.'; + return false; + } + } + } + + $FctInfo['type'] = 3; + + } + + } else { + if ( $FctCheck && ($this->FctPrefix!=='') && (strncmp($this->FctPrefix,$FctStr,strlen($this->FctPrefix))!==0) ) { + $ErrMsg = 'user function \''.$FctStr.'\' does not match the allowed prefix.'; return false; + } else if (!function_exists($FctStr)) { + $x = explode('.',$FctStr); + if (count($x)==2) { + if (class_exists($x[0])) { + $FctInfo = $x; + } else { + $ErrMsg = 'user function \''.$FctStr.'\' is not correct because \''.$x[0].'\' is not a class name.'; return false; + } + } else { + $ErrMsg = 'user function \''.$FctStr.'\' is not found.'; return false; + } + } + } + + if ($Save) $this->_UserFctLst[$FctId] = $FctInfo; + return true; + +} + +function meth_Misc_RunSubscript(&$CurrVal,$CurrPrm) { +// Run a subscript without any local variable damage + return @include($this->_Subscript); +} + +function meth_Misc_Charset($Charset) { + if ($Charset==='+') return; + $this->_CharsetFct = false; + if (is_string($Charset)) { + if (($Charset!=='') && ($Charset[0]==='=')) { + $ErrMsg = false; + $Charset = substr($Charset,1); + if ($this->meth_Misc_UserFctCheck($Charset,'f',$ErrMsg,$ErrMsg,false)) { + $this->_CharsetFct = true; + } else { + $this->meth_Misc_Alert('with charset option',$ErrMsg); + $Charset = ''; + } + } + } elseif (is_array($Charset)) { + $this->_CharsetFct = true; + } elseif ($Charset===false) { + $this->Protect = false; + } else { + $this->meth_Misc_Alert('with charset option','the option value is not a string nor an array.'); + $Charset = ''; + } + $this->Charset = $Charset; +} + +function meth_PlugIn_RunAll(&$FctBank,&$ArgLst) { + $OkAll = true; + foreach ($FctBank as $FctInfo) { + $Ok = call_user_func_array($FctInfo,$ArgLst); + if (!is_null($Ok)) $OkAll = ($OkAll && $Ok); + } + return $OkAll; +} + +function meth_PlugIn_Install($PlugInId,$ArgLst,$Auto) { + + $ErrMsg = 'with plug-in \''.$PlugInId.'\''; + + if (class_exists($PlugInId)) { + // Create an instance + $IsObj = true; + $PiRef = new $PlugInId; + $PiRef->TBS = &$this; // public $TBS property is madatory since PHP 8.2 + if (!method_exists($PiRef,'OnInstall')) return $this->meth_Misc_Alert($ErrMsg,'OnInstall() method is not found.'); + $FctRef = array(&$PiRef,'OnInstall'); + } else { + $FctRef = 'tbspi_'.$PlugInId.'_OnInstall'; + if(function_exists($FctRef)) { + $IsObj = false; + $PiRef = true; + } else { + return $this->meth_Misc_Alert($ErrMsg,'no class named \''.$PlugInId.'\' is found, and no function named \''.$FctRef.'\' is found.'); + } + } + + $this->_PlugIns[$PlugInId] = &$PiRef; + + $EventLst = call_user_func_array($FctRef,$ArgLst); + if (is_string($EventLst)) $EventLst = explode(',',$EventLst); + if (!is_array($EventLst)) return $this->meth_Misc_Alert($ErrMsg,'OnInstall() method does not return an array.'); + + // Add activated methods + foreach ($EventLst as $Event) { + $Event = trim($Event); + if (!$this->meth_PlugIn_SetEvent($PlugInId, $Event)) return false; + } + + return true; + +} + +function meth_PlugIn_SetEvent($PlugInId, $Event, $NewRef='') { +// Enable or disable a plug-in event. It can be called by a plug-in, even during the OnInstall event. $NewRef can be used to change the method associated to the event. + + // Check the event's name + if (strpos(',OnCommand,BeforeLoadTemplate,AfterLoadTemplate,BeforeShow,AfterShow,OnData,OnFormat,OnOperation,BeforeMergeBlock,OnMergeSection,OnMergeGroup,AfterMergeBlock,OnSpecialVar,OnMergeField,OnCacheField,', ','.$Event.',')===false) return $this->meth_Misc_Alert('with plug-in \''.$PlugInId.'\'','The plug-in event named \''.$Event.'\' is not supported by TinyButStrong (case-sensitive). This event may come from the OnInstall() method.'); + + $PropName = '_pi'.$Event; + + if ($NewRef===false) { + // Disable the event + if (!isset($this->$PropName)) return false; + $PropRef = &$this->$PropName; + unset($PropRef[$PlugInId]); + return true; + } + + // Prepare the reference to be called + $PiRef = &$this->_PlugIns[$PlugInId]; + if (is_object($PiRef)) { + if ($NewRef==='') $NewRef = $Event; + if (!method_exists($PiRef, $NewRef)) return $this->meth_Misc_Alert('with plug-in \''.$PlugInId.'\'','The plug-in event named \''.$Event.'\' is declared but its corresponding method \''.$NewRef.'\' is found.'); + $FctRef = array(&$PiRef, $NewRef); + } else { + $FctRef = ($NewRef==='') ? 'tbspi_'.$PlugInId.'_'.$Event : $NewRef; + if (!function_exists($FctRef)) return $this->meth_Misc_Alert('with plug-in \''.$PlugInId.'\'','The expected function \''.$FctRef.'\' is not found.'); + } + + // Save information into the corresponding property + if (!isset($this->$PropName)) $this->$PropName = array(); + $PropRef = &$this->$PropName; + $PropRef[$PlugInId] = $FctRef; + + // Flags saying if a plugin is installed + switch ($Event) { + case 'OnCommand': break; + case 'OnSpecialVar': break; + case 'OnOperation': break; + case 'OnFormat': $this->_piOnFrm_Ok = true; break; + default: $this->_PlugIns_Ok = true; break; + } + + return true; + +} + +/** + * Convert any value to a string without specific formating. + */ +static function meth_Misc_ToStr($Value) { + if (is_string($Value)) { + return $Value; + } elseif(is_object($Value)) { + if (method_exists($Value,'__toString')) { + return $Value->__toString(); + } elseif (is_a($Value, 'DateTime')) { + // ISO date-time format + return $Value->format('c'); + } + } + return @(string)$Value; // (string) is faster than strval() and settype() +} + +/** + * Return the formated representation of a Date/Time or numeric variable using a 'VB like' format syntax instead of the PHP syntax. + */ +function meth_Misc_Format(&$Value,&$PrmLst) { + + $FrmStr = $PrmLst['frm']; + $CheckNumeric = true; + if (is_string($Value)) $Value = trim($Value); + + if ($FrmStr==='') return ''; + $Frm = self::f_Misc_FormatSave($FrmStr); + + // Manage Multi format strings + if ($Frm['type']=='multi') { + + // Found the format according to the value (positive|negative|zero|null) + + if (is_numeric($Value)) { + // Numeric: + if (is_string($Value)) $Value = 0.0 + $Value; + if ($Value>0) { + $FrmStr = &$Frm[0]; + } elseif ($Value<0) { + $FrmStr = &$Frm[1]; + if ($Frm['abs']) $Value = abs($Value); + } else { + // zero + $FrmStr = &$Frm[2]; + $Minus = ''; + } + $CheckNumeric = false; + } else { + // date| + $Value = $this->meth_Misc_ToStr($Value); + if ($Value==='') { + // Null value + return $Frm[3]; + } else { + // Date conversion + $t = strtotime($Value); // We look if it's a date + if (($t===-1) || ($t===false)) { + // Date not recognized + return $Frm[1]; + } elseif ($t===943916400) { + // Date to zero in some softwares + return $Frm[2]; + } else { + // It's a date + $Value = $t; + $FrmStr = &$Frm[0]; + } + } + } + + // Retrieve the correct simple format + if ($FrmStr==='') return ''; + $Frm = self::f_Misc_FormatSave($FrmStr); + + } + + switch ($Frm['type']) { + case 'num': + // NUMERIC + if ($CheckNumeric) { + if (is_numeric($Value)) { + if (is_string($Value)) $Value = 0.0 + $Value; + } else { + return $this->meth_Misc_ToStr($Value); + } + } + if ($Frm['PerCent']) $Value = $Value * 100; + $Value = number_format($Value,$Frm['DecNbr'],$Frm['DecSep'],$Frm['ThsSep']); + if ($Frm['Pad']!==false) $Value = str_pad($Value, $Frm['Pad'], '0', STR_PAD_LEFT); + if ($Frm['ThsRpl']!==false) $Value = str_replace($Frm['ThsSep'], $Frm['ThsRpl'], $Value); + $Value = substr_replace($Frm['Str'],$Value,$Frm['Pos'],$Frm['Len']); + return $Value; + break; + case 'date': + // DATE + return $this->meth_Misc_DateFormat($Value, $Frm); + break; + default: + return $Frm['string']; + break; + } + +} + +function meth_Misc_DateFormat(&$Value, $Frm) { + + if (is_object($Value)) { + $Value = $this->meth_Misc_ToStr($Value); + } + + if ($Value==='') return ''; + + // Note : DateTime object is supported since PHP 5.2 + // So we could simplify this function using only DateTime instead of timestamp. + + // Now we try to get the timestamp + if (is_string($Value)) { + // Any string value is assumed to be a formated date. + // If you whant a string value to be a considered to a a time stamp, then use prefixe '@' accordding to the + $x = strtotime($Value); + // In case of error return false (return -1 for PHP < 5.1.0) + if (($x===false) || ($x===-1)) { + if (!is_numeric($Value)) { + // At this point the value is not recognized as a date + // Special fix for PHP 32-bit and date > '2038-01-19 03:14:07' => strtotime() failes + if (PHP_INT_SIZE === 4) { // 32-bit + try { + $date = new DateTime($Value); + return $date->format($Frm['str_us']); + // 'locale' cannot be supported in this case because strftime() has to equilavent with DateTime + } catch (Exception $e) { + // We take an arbitrary value in order to avoid formating error + $Value = 0; // '1970-01-01' + // echo $e->getMessage(); + } + } else { + // We take an arbirtary value in order to avoid formating error + $Value = 0; // '1970-01-01' + } + } + } else { + $Value = &$x; + } + } else { + if (!is_numeric($Value)) { + // It’s not a timestamp, thus we return the non formated value + return $this->meth_Misc_ToStr($Value); + } + } + + if ($Frm['loc'] || isset($PrmLst['locale'])) { + $x = strftime($Frm['str_loc'],$Value); + $this->meth_Conv_Str($x,false); // may have accent + return $x; + } else { + return date($Frm['str_us'],$Value); + } + +} + +/** + * Apply combo parameters. + * @param array $PrmLst The existing list of combo + * @param object|false $Loc The current locator, of false if called from an combo definition + */ +static function meth_Misc_ApplyPrmCombo(&$PrmLst, $Loc) { + + if (isset($PrmLst['combo'])) { + + $name_lst = explode(',', $PrmLst['combo']); + $DefLst = &$GLOBALS['_TBS_PrmCombo']; + + foreach ($name_lst as $name) { + if (isset($DefLst[$name])) { + $ap = $DefLst[$name]; + if (isset($PrmLst['ope']) && isset($ap['ope'])) { + $PrmLst['ope'] .= ',' . $ap['ope']; // ope will be processed fifo + unset($ap['ope']); + } + if ($Loc !== false) { + if ( isset($ap['if']) && is_array($ap['if']) ) { + foreach($ap['if'] as $v) { + self::f_Loc_PrmIfThen($Loc, true, $v, false); + } + unset($ap['if']); + } + if ( isset($ap['then']) && is_array($ap['then'])) { + foreach($ap['then'] as $v) { + self::f_Loc_PrmIfThen($Loc, false, $v, false); + } + unset($ap['then']); + } + } + $PrmLst = array_merge($ap, $PrmLst); + } else { + $this->meth_Misc_Alert("with parameter 'combo'", "Combo '". $a. "' is not yet set."); + } + } + + $PrmLst['_combo'] = $PrmLst['combo']; // for debug + unset($PrmLst['combo']); // for security + + } +} + +/** + * Simply update an array with another array. + * It works for both indexed or associativ arrays. + * NULL value will be deleted from the target array. + * + * @param array $array The target array to be updated. + * @param mixed $numerical True if the keys ar numerical. Use special keyword 'frm' for TBS formats, and 'prm' for a set of parameters. + * @param mixed $v An associative array of items to modify. Use value NULL for reset $array to an empty array. Other single value will be used with $d. + * @param mixed $d To be used when $v is a single not null value. Will apply the key $v with value $d. + */ + static function f_Misc_UpdateArray(&$array, $numerical, $v, $d) { + if (!is_array($v)) { + if (is_null($v)) { + $array = array(); + return; + } else { + $v = array($v=>$d); + } + } + foreach ($v as $p=>$a) { + if ($numerical===true) { // numerical keys + if (is_string($p)) { + // syntax: item => true/false + $i = array_search($p, $array, true); + if ($i===false) { + if (!is_null($a)) $array[] = $p; + } else { + if (is_null($a)) array_splice($array, $i, 1); + } + } else { + // syntax: i => item + $i = array_search($a, $array, true); + if ($i==false) $array[] = $a; + } + } else { // string keys + if (is_null($a)) { + unset($array[$p]); + } elseif ($numerical==='frm') { + self::f_Misc_FormatSave($a, $p); + } else { + if ($numerical==='prm') { + // apply existing combo on the new combo, so that all combo are translated into basic parameters + if ( isset($a['if']) && (!is_array($a['if'])) ) { + $a['if'] = array($a['if']); + } + if ( isset($a['then']) && (!is_array($a['then'])) ) { + $a['then'] = array($a['then']); + } + self::meth_Misc_ApplyPrmCombo($a, false); + } + $array[$p] = $a; + } + } + } +} + +static function f_Misc_FormatSave(&$FrmStr,$Alias='') { + + $FormatLst = &$GLOBALS['_TBS_FormatLst']; + + if (isset($FormatLst[$FrmStr])) { + if ($Alias!='') $FormatLst[$Alias] = &$FormatLst[$FrmStr]; + return $FormatLst[$FrmStr]; + } + + if (strpos($FrmStr,'|')!==false) { + + // Multi format + $Frm = explode('|',$FrmStr); // syntax: Postive|Negative|Zero|Null + $FrmNbr = count($Frm); + $Frm['abs'] = ($FrmNbr>1); + if ($FrmNbr<3) $Frm[2] = &$Frm[0]; // zero + if ($FrmNbr<4) $Frm[3] = ''; // null + $Frm['type'] = 'multi'; + $FormatLst[$FrmStr] = $Frm; + + } elseif (($nPosEnd = strrpos($FrmStr,'0'))!==false) { + + // Numeric format + $nDecSep = '.'; + $nDecNbr = 0; + $nDecOk = true; + $nPad = false; + $nPadZ = 0; + + if (substr($FrmStr,$nPosEnd+1,1)==='.') { + $nPosEnd++; + $nPos = $nPosEnd; + $nPadZ = 1; + } else { + $nPos = $nPosEnd - 1; + while (($nPos>=0) && ($FrmStr[$nPos]==='0')) { + $nPos--; + } + if (($nPos>=1) && ($FrmStr[$nPos-1]==='0')) { + $nDecSep = $FrmStr[$nPos]; + $nDecNbr = $nPosEnd - $nPos; + } else { + $nDecOk = false; + } + } + + // Thousand separator + $nThsSep = ''; + $nThsRpl = false; + if (($nDecOk) && ($nPos>=5)) { + if ((substr($FrmStr,$nPos-3,3)==='000') && ($FrmStr[$nPos-4]!=='0')) { + $p = strrpos(substr($FrmStr,0,$nPos-4), '0'); + if ($p!==false) { + $len = $nPos-4-$p; + $x = substr($FrmStr, $p+1, $len); + if ($len>1) { + // for compatibility for number_format() with PHP < 5.4.0 + $nThsSep = ($nDecSep=='*') ? '.' : '*'; + $nThsRpl = $x; + } else { + $nThsSep = $x; + } + $nPos = $p+1; + } + } + } + + // Pass next zero + if ($nDecOk) $nPos--; + while (($nPos>=0) && ($FrmStr[$nPos]==='0')) { + $nPos--; + } + + $nLen = $nPosEnd-$nPos; + if ( ($nThsSep==='') && ($nLen>($nDecNbr+$nPadZ+1)) ) $nPad = $nLen - $nPadZ; + + // Percent + $nPerCent = (strpos($FrmStr,'%')===false) ? false : true; + + $FormatLst[$FrmStr] = array('type'=>'num','Str'=>$FrmStr,'Pos'=>($nPos+1),'Len'=>$nLen,'ThsSep'=>$nThsSep,'ThsRpl'=>$nThsRpl,'DecSep'=>$nDecSep,'DecNbr'=>$nDecNbr,'PerCent'=>$nPerCent,'Pad'=>$nPad); + + } else { + + // Date format + $x = $FrmStr; + $FrmPHP = ''; + $FrmLOC = ''; + $StrIn = false; + $Cnt = 0; + $i = strpos($FrmStr,'(locale)'); + $Locale = ($i!==false); + if ($Locale) $x = substr_replace($x,'',$i,8); + + $iEnd = strlen($x); + for ($i=0;$i<$iEnd;$i++) { + + if ($StrIn) { + // We are in a string part + if ($x[$i]==='"') { + if (substr($x,$i+1,1)==='"') { + $FrmPHP .= '\\"'; // protected char + $FrmLOC .= $x[$i]; + $i++; + } else { + $StrIn = false; + } + } else { + $FrmPHP .= '\\'.$x[$i]; // protected char + $FrmLOC .= $x[$i]; + } + } else { + if ($x[$i]==='"') { + $StrIn = true; + } else { + $Cnt++; + if (strcasecmp(substr($x,$i,2),'hh' )===0) { $FrmPHP .= 'H'; $FrmLOC .= '%H'; $i += 1;} + elseif (strcasecmp(substr($x,$i,2),'hm' )===0) { $FrmPHP .= 'h'; $FrmLOC .= '%I'; $i += 1;} // for compatibility + elseif (strcasecmp(substr($x,$i,1),'h' )===0) { $FrmPHP .= 'G'; $FrmLOC .= '%H';} + elseif (strcasecmp(substr($x,$i,2),'rr' )===0) { $FrmPHP .= 'h'; $FrmLOC .= '%I'; $i += 1;} + elseif (strcasecmp(substr($x,$i,1),'r' )===0) { $FrmPHP .= 'g'; $FrmLOC .= '%I';} + elseif (strcasecmp(substr($x,$i,4),'ampm')===0) { $FrmPHP .= substr($x,$i,1); $FrmLOC .= '%p'; $i += 3;} // $Fmp = 'A' or 'a' + elseif (strcasecmp(substr($x,$i,2),'nn' )===0) { $FrmPHP .= 'i'; $FrmLOC .= '%M'; $i += 1;} + elseif (strcasecmp(substr($x,$i,2),'ss' )===0) { $FrmPHP .= 's'; $FrmLOC .= '%S'; $i += 1;} + elseif (strcasecmp(substr($x,$i,2),'xx' )===0) { $FrmPHP .= 'S'; $FrmLOC .= '' ; $i += 1;} + elseif (strcasecmp(substr($x,$i,4),'yyyy')===0) { $FrmPHP .= 'Y'; $FrmLOC .= '%Y'; $i += 3;} + elseif (strcasecmp(substr($x,$i,2),'yy' )===0) { $FrmPHP .= 'y'; $FrmLOC .= '%y'; $i += 1;} + elseif (strcasecmp(substr($x,$i,4),'mmmm')===0) { $FrmPHP .= 'F'; $FrmLOC .= '%B'; $i += 3;} + elseif (strcasecmp(substr($x,$i,3),'mmm' )===0) { $FrmPHP .= 'M'; $FrmLOC .= '%b'; $i += 2;} + elseif (strcasecmp(substr($x,$i,2),'mm' )===0) { $FrmPHP .= 'm'; $FrmLOC .= '%m'; $i += 1;} + elseif (strcasecmp(substr($x,$i,1),'m' )===0) { $FrmPHP .= 'n'; $FrmLOC .= '%m';} + elseif (strcasecmp(substr($x,$i,4),'wwww')===0) { $FrmPHP .= 'l'; $FrmLOC .= '%A'; $i += 3;} + elseif (strcasecmp(substr($x,$i,3),'www' )===0) { $FrmPHP .= 'D'; $FrmLOC .= '%a'; $i += 2;} + elseif (strcasecmp(substr($x,$i,1),'w' )===0) { $FrmPHP .= 'w'; $FrmLOC .= '%u';} + elseif (strcasecmp(substr($x,$i,4),'dddd')===0) { $FrmPHP .= 'l'; $FrmLOC .= '%A'; $i += 3;} + elseif (strcasecmp(substr($x,$i,3),'ddd' )===0) { $FrmPHP .= 'D'; $FrmLOC .= '%a'; $i += 2;} + elseif (strcasecmp(substr($x,$i,2),'dd' )===0) { $FrmPHP .= 'd'; $FrmLOC .= '%d'; $i += 1;} + elseif (strcasecmp(substr($x,$i,1),'d' )===0) { $FrmPHP .= 'j'; $FrmLOC .= '%d';} + else { + $FrmPHP .= '\\'.$x[$i]; // protected char + $FrmLOC .= $x[$i]; // protected char + $Cnt--; + } + } + } + + } + + if ($Cnt>0) { + $FormatLst[$FrmStr] = array('type'=>'date','str_us'=>$FrmPHP,'str_loc'=>$FrmLOC,'loc'=>$Locale); + } else { + $FormatLst[$FrmStr] = array('type'=>'else','string'=>$FrmStr); + } + + } + + if ($Alias!='') $FormatLst[$Alias] = &$FormatLst[$FrmStr]; + + return $FormatLst[$FrmStr]; + +} + +static function f_Misc_ConvSpe(&$Loc) { + if ($Loc->ConvMode!==2) { + $Loc->ConvMode = 2; + $Loc->ConvEsc = false; + $Loc->ConvWS = false; + $Loc->ConvJS = false; + $Loc->ConvUrl = false; + $Loc->ConvUtf8 = false; + } +} + +/** + * Return the information if parsing a form which can be either a property of a function. + * @param string $Str The form. Example : 'my_func(aaa,bbb)' + * @return array Information about the form. Example : array('name' => 'my_func', 'as_fct' => true, 'args' => array('aaa', 'bbb'),) + * name: the name of the function of the property. + * as_fct: true if the form is as a function + * args: arguments of the function, or empty array if it's a property + */ +static function f_Misc_ParseFctForm($Str) { + $info = array('name' => $Str, 'as_fct' => false, 'args' => array()); + if (substr($Str,-1,1)===')') { + $pos = strpos($Str,'('); + if ($pos!==false) { + $info['args'] = explode(',',substr($Str,$pos+1,strlen($Str)-$pos-2)); + $info['name'] = substr($Str,0,$pos); + $info['as_fct'] = true; + } + } + return $info; +} + +/** + * Check if a string condition is true. + * @param string $Str The condition to check. + * @return boolean True if the condition if checked. + */ +static function f_Misc_CheckCondition($Str) { +// Check if an expression like "exrp1=expr2" is true or false. + + // Bluid $StrZ, wich is the same as $Str but with 'z' for each character that is protected with "'". + // This will help to search for operators outside protected strings. + $StrZ = $Str; + $Max = strlen($Str)-1; + $p = strpos($Str,'\''); + if ($Esc=($p!==false)) { + $In = true; + for ($p=$p+1;$p<=$Max;$p++) { + if ($StrZ[$p]==='\'') { + $In = !$In; + } elseif ($In) { + $StrZ[$p] = 'z'; + } + } + } + + // Find operator and position + $Ope = '='; + $Len = 1; + $p = strpos($StrZ,$Ope); + if ($p===false) { + $Ope = '+'; + $p = strpos($StrZ,$Ope); + if ($p===false) return false; + if (($p>0) && ($StrZ[$p-1]==='-')) { + $Ope = '-+'; $p--; $Len=2; + } elseif (($p<$Max) && ($StrZ[$p+1]==='-')) { + $Ope = '+-'; $Len=2; + } else { + return false; + } + } else { + if ($p>0) { + $x = $StrZ[$p-1]; + if ($x==='!') { + $Ope = '!='; $p--; $Len=2; + } elseif ($x==='~') { + $Ope = '~='; $p--; $Len=2; + } elseif ($p<$Max) { + $y = $StrZ[$p+1]; + if ($y==='=') { + $Len=2; + } elseif (($x==='+') && ($y==='-')) { + $Ope = '+=-'; $p--; $Len=3; + } elseif (($x==='-') && ($y==='+')) { + $Ope = '-=+'; $p--; $Len=3; + } + } else { + } + } + } + + // Read values + $Val1 = trim(substr($Str,0,$p)); + $Val2 = trim(substr($Str,$p+$Len)); + if ($Esc) { + $NoDelim1 = self::f_Misc_DelDelimiter($Val1,'\''); + $NoDelim2 = self::f_Misc_DelDelimiter($Val2,'\''); + } else { + $NoDelim1 = $NoDelim2 = false; + } + + // Compare values + if ($Ope==='=') { + return (strcasecmp($Val1,$Val2)==0); + } elseif ($Ope==='!=') { + return (strcasecmp($Val1,$Val2)!=0); + } elseif ($Ope==='~=') { + return (preg_match($Val2,$Val1)>0); + } else { + // If a value has no string delimiter, we assume it is supposed to be a numerical comparison. + if ($NoDelim1 && ($Val1 === '') ) $Val1 = '0'; + if ($NoDelim2 && ($Val2 === '') ) $Val2 ='0'; + // PHP makes a numerical comparison when each item is independently either a numeric value or a numeric string. Otherwise it makes a string comparison. + // So we let PHP doing the comparison on its onw way. + if ($Ope==='+-') { + return ($Val1 > $Val2); + } elseif ($Ope==='-+') { + return ($Val1 < $Val2); + } elseif ($Ope==='+=-') { + return ($Val1 >= $Val2); + } elseif ($Ope==='-=+') { + return ($Val1<=$Val2); + } else { + return false; + } + } + +} + +/** + * Delete the string delimiters that surrounds the string, if any. But not inside (no need). + * @param string $Txt The string to modifiy. + * @param string $Delim The character that can delimit the string. + * @return boolean True if the given string was not delimited with $Delim. + */ +static function f_Misc_DelDelimiter(&$Txt,$Delim) { +// Delete the string delimiters + $len = strlen($Txt); + if (($len>1) && ($Txt[0]===$Delim)) { + if ($Txt[$len-1]===$Delim) $Txt = substr($Txt,1,$len-2); + return false; + } else { + return true; + } +} + +static function f_Misc_GetFile(&$Res, &$File, $LastFile='', $IncludePath=false, $Contents=true) { +// Load the content of a file into the text variable. + + $Res = ''; + $fd = self::f_Misc_TryFile($File, false); + if ($fd===false) { + if (is_array($IncludePath)) { + foreach ($IncludePath as $d) { + $fd = self::f_Misc_TryFile($File, $d); + if ($fd!==false) break; + } + } + if (($fd===false) && ($LastFile!='')) $fd = self::f_Misc_TryFile($File, dirname($LastFile)); + if ($fd===false) return false; + } + + $fs = fstat($fd); + if ($Contents) { + // Return contents + if (isset($fs['size'])) { + if ($fs['size']>0) $Res = fread($fd,$fs['size']); + } else { + while (!feof($fd)) $Res .= fread($fd,4096); + } + } else { + // Return stats + $Res = $fs; + } + + fclose($fd); + return true; + +} + +/** + * Try to open the file for reading. + * @param string $File The file name. + * @param string|bolean $Dir A The directory where to search, of false to omit directory. + * @return ressource Return the file pointer, of false on error. Note that urgument $File will be updated to the file with directory. + */ +static function f_Misc_TryFile(&$File, $Dir) { + if ($Dir==='') return false; + $FileSearch = ($Dir===false) ? $File : $Dir.'/'.$File; + // 'rb' if binary for some OS. fopen() uses include_path and search on the __FILE__ directory while file_exists() doesn't. + $f = @fopen($FileSearch, 'r', true); + if ($f!==false) $File = $FileSearch; + return $f; +} + +/** + * Read TBS or XML tags, starting to the begining of the tag. + */ +static function f_Loc_PrmRead(&$Txt,$Pos,$XmlTag,$DelimChrs,$BegStr,$EndStr,&$Loc,&$PosEnd,$WithPos=false) { + + $BegLen = strlen($BegStr); + $BegChr = $BegStr[0]; + $BegIs1 = ($BegLen===1); + + $DelimIdx = false; + $DelimCnt = 0; + $DelimChr = ''; + $BegCnt = 0; + $SubName = $Loc->SubOk; + + $Status = 0; // 0: name not started, 1: name started, 2: name ended, 3: equal found, 4: value started + $PosName = 0; + $PosNend = 0; + $PosVal = 0; + + // Variables for checking the loop + $PosEnd = strpos($Txt,$EndStr,$Pos); + if ($PosEnd===false) return; + $Continue = ($Pos<$PosEnd); + + while ($Continue) { + + $Chr = $Txt[$Pos]; + + if ($DelimIdx) { // Reading in the string + + if ($Chr===$DelimChr) { // Quote found + if ($Chr===$Txt[$Pos+1]) { // Double Quote => the string continue with un-double the quote + $Pos++; + } else { // Simple Quote => end of string + $DelimIdx = false; + } + } + + } else { // Reading outside the string + + if ($BegCnt===0) { + + // Analyzing parameters + $CheckChr = false; + if (($Chr===' ') || ($Chr==="\r") || ($Chr==="\n")) { + if ($Status===1) { + if ($SubName && ($XmlTag===false)) { + // Accept spaces in TBS subname. + } else { + $Status = 2; + $PosNend = $Pos; + } + } elseif ($XmlTag && ($Status===4)) { + self::f_Loc_PrmCompute($Txt,$Loc,$SubName,$Status,$XmlTag,$DelimChr,$DelimCnt,$PosName,$PosNend,$PosVal,$Pos,$WithPos); + $Status = 0; + } + } elseif (($XmlTag===false) && ($Chr===';')) { + self::f_Loc_PrmCompute($Txt,$Loc,$SubName,$Status,$XmlTag,$DelimChr,$DelimCnt,$PosName,$PosNend,$PosVal,$Pos,$WithPos); + $Status = 0; + } elseif ($Status===4) { + $CheckChr = true; + } elseif ($Status===3) { + $Status = 4; + $DelimCnt = 0; + $PosVal = $Pos; + $CheckChr = true; + } elseif ($Status===2) { + if ($Chr==='=') { + $Status = 3; + } elseif ($XmlTag) { + self::f_Loc_PrmCompute($Txt,$Loc,$SubName,$Status,$XmlTag,$DelimChr,$DelimCnt,$PosName,$PosNend,$PosVal,$Pos,$WithPos); + $Status = 1; + $PosName = $Pos; + $CheckChr = true; + } else { + $Status = 4; + $DelimCnt = 0; + $PosVal = $Pos; + $CheckChr = true; + } + } elseif ($Status===1) { + if ($Chr==='=') { + $Status = 3; + $PosNend = $Pos; + } else { + $CheckChr = true; + } + } else { + $Status = 1; + $PosName = $Pos; + $CheckChr = true; + } + + if ($CheckChr) { + $DelimIdx = strpos($DelimChrs,$Chr); + if ($DelimIdx===false) { + if ($Chr===$BegChr) { + if ($BegIs1) { + $BegCnt++; + } elseif(substr($Txt,$Pos,$BegLen)===$BegStr) { + $BegCnt++; + } + } + } else { + $DelimChr = $DelimChrs[$DelimIdx]; + $DelimCnt++; + $DelimIdx = true; + } + } + + } else { + if ($Chr===$BegChr) { + if ($BegIs1) { + $BegCnt++; + } elseif(substr($Txt,$Pos,$BegLen)===$BegStr) { + $BegCnt++; + } + } + } + + } + + // Next char + $Pos++; + + // We check if it's the end + if ($Pos===$PosEnd) { + if ($XmlTag) { + $Continue = false; + } elseif ($DelimIdx===false) { + if ($BegCnt>0) { + $BegCnt--; + } else { + $Continue = false; + } + } + if ($Continue) { + $PosEnd = strpos($Txt,$EndStr,$PosEnd+1); + if ($PosEnd===false) return; + } else { + if ($XmlTag && ($Txt[$Pos-1]==='/')) $Pos--; // In case last attribute is stuck to "/>" + self::f_Loc_PrmCompute($Txt,$Loc,$SubName,$Status,$XmlTag,$DelimChr,$DelimCnt,$PosName,$PosNend,$PosVal,$Pos,$WithPos); + } + } + + } + + $PosEnd = $PosEnd + (strlen($EndStr)-1); + +} + +static function f_Loc_PrmCompute(&$Txt,&$Loc,&$SubName,$Status,$XmlTag,$DelimChr,$DelimCnt,$PosName,$PosNend,$PosVal,$Pos,$WithPos) { + + if ($Status===0) { + $SubName = false; + } else { + if ($Status===1) { + $x = substr($Txt,$PosName,$Pos-$PosName); + } else { + $x = substr($Txt,$PosName,$PosNend-$PosName); + } + if ($XmlTag) $x = strtolower($x); + if ($SubName) { + $Loc->SubName = trim($x); + $SubName = false; + } else { + if ($Status===4) { + $v = trim(substr($Txt,$PosVal,$Pos-$PosVal)); + if ($DelimCnt===1) { // Delete quotes inside the value + if ($v[0]===$DelimChr) { + $len = strlen($v); + if ($v[$len-1]===$DelimChr) { + $v = substr($v,1,$len-2); + $v = str_replace($DelimChr.$DelimChr,$DelimChr,$v); + } + } + } + } else { + $v = true; + } + if ($x==='if') { + self::f_Loc_PrmIfThen($Loc, true, $v, true); + } elseif ($x==='then') { + self::f_Loc_PrmIfThen($Loc, false, $v, true); + } else { + $Loc->PrmLst[$x] = $v; + if ($WithPos) $Loc->PrmPos[$x] = array($PosName,$PosNend,$PosVal,$Pos,$DelimChr,$DelimCnt); + } + } + } + +} + +/** + * Add a new parameter 'if or 'then' to the locator. + * + * @param object $Loc The locator. + * @param boolean $IsIf Concerned parameter. True means 'if', false means 'then'. + * @param string $Val The value of the parameter. + * @param boolean $Ordered True means the parameter comes from the template and order must be checked. False means it comes from PHP and order is free. + * + */ +static function f_Loc_PrmIfThen(&$Loc, $IsIf, $Val, $Ordered) { + $nb_if = &$Loc->PrmIfNbr; + if ($nb_if===false) { + $nb_if = 0; + $Loc->PrmIf = array(); + $Loc->PrmIfVar = array(); + $Loc->PrmThen = array(); + $Loc->PrmThenVar = array(); + $Loc->PrmElseVar = true; + } + if ($IsIf) { + $nb_if++; + $Loc->PrmIf[$nb_if] = $Val; + $Loc->PrmIfVar[$nb_if] = true; + } else { + if ($Ordered) { + $nb_then = $nb_if; + if ($nb_then===false) $nb_then = 1; // Only the first 'then' can be placed before its 'if'. This is for compatibility. + } else { + $nb_then = count($Loc->PrmThen) + 1; + } + $Loc->PrmThen[$nb_then] = $Val; + $Loc->PrmThenVar[$nb_then] = true; + } +} + +/* +This function enables to enlarge the pos limits of the Locator. +If the search result is not correct, $PosBeg must not change its value, and $PosEnd must be False. +This is because of the calling function. +*/ +static function f_Loc_EnlargeToStr(&$Txt,&$Loc,$StrBeg,$StrEnd) { + + // Search for the begining string + $Pos = $Loc->PosBeg; + $Ok = false; + do { + $Pos = strrpos(substr($Txt,0,$Pos),$StrBeg[0]); + if ($Pos!==false) { + if (substr($Txt,$Pos,strlen($StrBeg))===$StrBeg) $Ok = true; + } + } while ( (!$Ok) && ($Pos!==false) ); + + if ($Ok) { + $PosEnd = strpos($Txt,$StrEnd,$Loc->PosEnd + 1); + if ($PosEnd===false) { + $Ok = false; + } else { + $Loc->PosBeg = $Pos; + $Loc->PosEnd = $PosEnd + strlen($StrEnd) - 1; + } + } + + return $Ok; + +} + +static function f_Loc_EnlargeToTag(&$Txt,&$Loc,$TagStr,$RetInnerSrc) { +//Modify $Loc, return false if tags not found, returns the inner source of tag if $RetInnerSrc=true + + $AliasLst = &$GLOBALS['_TBS_BlockAlias']; + + // Analyze string + $Ref = 0; + $LevelStop = 0; + $i = 0; + $TagFct = array(); + $TagLst = array(); + $TagBnd = array(); + while ($TagStr!=='') { + // get next tag + $p = strpos($TagStr, '+'); + if ($p===false) { + $t = $TagStr; + $TagStr = ''; + } else { + $t = substr($TagStr,0,$p); + $TagStr = substr($TagStr,$p+1); + } + // Check parentheses, relative position and single tag + do { + $t = trim($t); + $e = strlen($t) - 1; // pos of last char + if (($e>1) && ($t[0]==='(') && ($t[$e]===')')) { + if ($Ref===0) $Ref = $i; + if ($Ref===$i) $LevelStop++; + $t = substr($t,1,$e-1); + } else { + if (($e>=0) && ($t[$e]==='/')) $t = substr($t,0,$e); // for compatibilty + $e = false; + } + } while ($e!==false); + // Check for multiples + $p = strpos($t, '*'); + if ($p!==false) { + $n = intval(substr($t, 0, $p)); + $t = substr($t, $p + 1); + $n = max($n ,1); // prevent for error: minimum valu is 1 + $TagStr = str_repeat($t . '+', $n-1) . $TagStr; + } + // Reference + if (($t==='.') && ($Ref===0)) $Ref = $i; + // Take of the (!) prefix + $b = ''; + if (($t!=='') && ($t[0]==='!')) { + $t = substr($t, 1); + $b = '!'; + } + // Block alias + $a = false; + if (isset($AliasLst[$t])) { + $a = $AliasLst[$t]; // a string or a function + if (is_string($a)) { + if ($i>999) return false; // prevent from circular alias + $TagStr = $b . $a . (($TagStr==='') ? '' : '+') . $TagStr; + $t = false; + } + } + if ($t!==false) { + $TagLst[$i] = $t; // with prefix ! if specified + $TagFct[$i] = $a; + $TagBnd[$i] = ($b===''); + $i++; + } + } + + $TagMax = $i-1; + + // Find tags that embeds the locator + if ($LevelStop===0) $LevelStop = 1; + + // First tag of reference + if ($TagLst[$Ref] === '.') { + $TagO = new clsTbsLocator; + $TagO->PosBeg = $Loc->PosBeg; + $TagO->PosEnd = $Loc->PosEnd; + $PosBeg = $Loc->PosBeg; + $PosEnd = $Loc->PosEnd; + } else { + $TagO = self::f_Loc_Enlarge_Find($Txt,$TagLst[$Ref],$TagFct[$Ref],$Loc->PosBeg-1,false,$LevelStop); + if ($TagO===false) return false; + $PosBeg = $TagO->PosBeg; + $LevelStop += -$TagO->RightLevel; // RightLevel=1 only if the tag is single and embeds $Loc, otherwise it is 0 + if ($LevelStop>0) { + $TagC = self::f_Loc_Enlarge_Find($Txt,$TagLst[$Ref],$TagFct[$Ref],$Loc->PosEnd+1,true,-$LevelStop); + if ($TagC==false) return false; + $PosEnd = $TagC->PosEnd; + $InnerLim = $TagC->PosBeg; + if ((!$TagBnd[$Ref]) && ($TagMax==0)) { + $PosBeg = $TagO->PosEnd + 1; + $PosEnd = $TagC->PosBeg - 1; + } + } else { + $PosEnd = $TagO->PosEnd; + $InnerLim = $PosEnd + 1; + } + } + + $RetVal = true; + if ($RetInnerSrc) { + $RetVal = ''; + if ($Loc->PosBeg>$TagO->PosEnd) $RetVal .= substr($Txt,$TagO->PosEnd+1,min($Loc->PosBeg,$InnerLim)-$TagO->PosEnd-1); + if ($Loc->PosEnd<$InnerLim) $RetVal .= substr($Txt,max($Loc->PosEnd,$TagO->PosEnd)+1,$InnerLim-max($Loc->PosEnd,$TagO->PosEnd)-1); + } + + // Other tags forward + $TagC = true; + for ($i=$Ref+1;$i<=$TagMax;$i++) { + $x = $TagLst[$i]; + if (($x!=='') && ($TagC!==false)) { + $level = ($TagBnd[$i]) ? 0 : 1; + $TagC = self::f_Loc_Enlarge_Find($Txt,$x,$TagFct[$i],$PosEnd+1,true,$level); + if ($TagC!==false) { + $PosEnd = ($TagBnd[$i]) ? $TagC->PosEnd : $TagC->PosBeg -1 ; + } + } + } + + // Other tags backward + $TagO = true; + for ($i=$Ref-1;$i>=0;$i--) { + $x = $TagLst[$i]; + if (($x!=='') && ($TagO!==false)) { + $level = ($TagBnd[$i]) ? 0 : -1; + $TagO = self::f_Loc_Enlarge_Find($Txt,$x,$TagFct[$i],$PosBeg-1,false,$level); + if ($TagO!==false) { + $PosBeg = ($TagBnd[$i]) ? $TagO->PosBeg : $TagO->PosEnd + 1; + } + } + } + + $Loc->PosBeg = $PosBeg; + $Loc->PosEnd = $PosEnd; + return $RetVal; + +} + +static function f_Loc_Enlarge_Find($Txt, $Tag, $Fct, $Pos, $Forward, $LevelStop) { + if ($Fct===false) { + return self::f_Xml_FindTag($Txt,$Tag,(!$Forward),$Pos,$Forward,$LevelStop,false); + } else { + $p = call_user_func_array($Fct,array($Tag,$Txt,$Pos,$Forward,$LevelStop)); + if ($p===false) { + return false; + } else { + return (object) array('PosBeg'=>$p, 'PosEnd'=>$p, 'RightLevel'=> 0); // it's a trick + } + } +} + +/** + * Return the expected value for a boolean attribute + */ +static function f_Loc_AttBoolean($CurrVal, $AttTrue, $AttName) { + + if ($AttTrue===true) { + if (self::meth_Misc_ToStr($CurrVal)==='') { + return ''; + } else { + return $AttName; + } + } elseif (self::meth_Misc_ToStr($CurrVal)===$AttTrue) { + return $AttName; + } else { + return ''; + } + +} + +/** + * Affects the positions of a list of locators regarding to a specific moving locator. + */ +static function f_Loc_Moving(&$LocM, &$LocLst) { + foreach ($LocLst as &$Loc) { + if ($Loc !== $LocM) { + if ($Loc->PosBeg >= $LocM->InsPos) { + $Loc->PosBeg += $LocM->InsLen; + $Loc->PosEnd += $LocM->InsLen; + } + if ($Loc->PosBeg > $LocM->DelPos) { + $Loc->PosBeg -= $LocM->DelLen; + $Loc->PosEnd -= $LocM->DelLen; + } + } + } + return true; +} + +/** + * Sort the locators in the list. Apply the bubble algorithm. + * Deleted locators maked with DelMe. + * @param array $LocLst An array of locators. + * @param boolean $DelEmbd True to deleted locators that embded other ones. + * @param boolean $iFirst Index of the first item. + * @return integer Return the number of met embedding locators. + */ +static function f_Loc_Sort(&$LocLst, $DelEmbd, $iFirst = 0) { + + $iLast = $iFirst + count($LocLst) - 1; + $embd = 0; + + for ($i = $iLast ; $i>=$iFirst ; $i--) { + $Loc = $LocLst[$i]; + $d = (isset($Loc->DelMe) && $Loc->DelMe); + $b = $Loc->PosBeg; + $e = $Loc->PosEnd; + for ($j=$i+1; $j<=$iLast ; $j++) { + // If DelMe, then the loc will be put at the end and deleted + $jb = $LocLst[$j]->PosBeg; + if ($d || ($b > $jb)) { + $LocLst[$j-1] = $LocLst[$j]; + $LocLst[$j] = $Loc; + } elseif ($e > $jb) { + $embd++; + if ($DelEmbd) { + $d = true; + $j--; // replay the current position + } else { + $j = $iLast; // quit the loop + } + } else { + $j = $iLast; // quit the loop + } + } + if ($d) { + unset($LocLst[$iLast]); + $iLast--; + } + } + + return $embd; +} + +/** + * Prepare all informations to move a locator according to parameter "att". + * + * @param false|true|array $MoveLocLst true to simple move the loc, or an array of loc to rearrange the list after the move. + * Note: rearrange doest not work with PHP4. + */ +static function f_Xml_AttFind(&$Txt,&$Loc,$MoveLocLst=false,$AttDelim=false,$LocLst=false) { +// att=div#class ; att=((div))#class ; att=+((div))#class + + $Att = $Loc->PrmLst['att']; + unset($Loc->PrmLst['att']); // prevent from processing the field twice + $Loc->PrmLst['att;'] = $Att; // for debug + + $p = strrpos($Att,'#'); + if ($p===false) { + $TagLst = ''; + } else { + $TagLst = substr($Att,0,$p); + $Att = substr($Att,$p+1); + } + + $Forward = (substr($TagLst,0,1)==='+'); + if ($Forward) $TagLst = substr($TagLst,1); + $TagLst = explode('+',$TagLst); + + $iMax = count($TagLst)-1; + $WithPrm = false; + $LocO = &$Loc; + foreach ($TagLst as $i=>$Tag) { + $LevelStop = false; + while ((strlen($Tag)>1) && (substr($Tag,0,1)==='(') && (substr($Tag,-1,1)===')')) { + if ($LevelStop===false) $LevelStop = 0; + $LevelStop++; + $Tag = trim(substr($Tag,1,strlen($Tag)-2)); + } + if ($i==$iMax) $WithPrm = true; + $Pos = ($Forward) ? $LocO->PosEnd+1 : $LocO->PosBeg-1; + unset($LocO); + $LocO = self::f_Xml_FindTag($Txt,$Tag,true,$Pos,$Forward,$LevelStop,$WithPrm,$WithPrm); + if ($LocO===false) return false; + } + + $Loc->AttForward = $Forward; + $Loc->AttTagBeg = $LocO->PosBeg; + $Loc->AttTagEnd = $LocO->PosEnd; + $Loc->AttDelimChr = false; + + if ($Att==='.') { + // this indicates that the TBS field is supposed to be inside an attribute's value + foreach ($LocO->PrmPos as $a=>$p ) { + if ( ($p[0]<$Loc->PosBeg) && ($Loc->PosEnd<$p[3]) ) $Att = $a; + } + if ($Att==='.') return false; + } + $Loc->AttName = $Att; + + $AttLC = strtolower($Att); + if (isset($LocO->PrmLst[$AttLC])) { + // The attribute is existing + $p = $LocO->PrmPos[$AttLC]; + $Loc->AttBeg = $p[0]; + $p[3]--; while ($Txt[$p[3]]===' ') $p[3]--; // external end of the attribute, may has an extra spaces + $Loc->AttEnd = $p[3]; + $Loc->AttDelimCnt = $p[5]; + $Loc->AttDelimChr = $p[4]; + if (($p[1]>$p[0]) && ($p[2]>$p[1])) { + //$Loc->AttNameEnd = $p[1]; + $Loc->AttValBeg = $p[2]; + } else { // attribute without value + //$Loc->AttNameEnd = $p[3]; + $Loc->AttValBeg = false; + } + } else { + // The attribute is not yet existing + $Loc->AttDelimCnt = 0; + $Loc->AttBeg = false; + } + + // Search for a delimitor + if (($Loc->AttDelimCnt==0) && (isset($LocO->PrmPos))) { + foreach ($LocO->PrmPos as $p) { + if ($p[5]>0) $Loc->AttDelimChr = $p[4]; + } + } + + if ($MoveLocLst) return self::f_Xml_AttMove($Txt,$Loc,$AttDelim,$MoveLocLst); + + return true; + +} + +/** + * Move a locator in the source from its original location to the attribute location. + * The new locator string is only '[]', no need to copy the full source since all parameters are saved in $Loc.* + * + * @param false|true|array $MoveLocLst If the function is called from the caching process, then this value is an array. + */ +static function f_Xml_AttMove(&$Txt, &$Loc, $AttDelim, &$MoveLocLst) { + + if ($AttDelim===false) $AttDelim = $Loc->AttDelimChr; + if ($AttDelim===false) $AttDelim = '"'; + + $DelPos = $Loc->PosBeg; + $DelLen = $Loc->PosEnd - $Loc->PosBeg + 1; + $Txt = substr_replace($Txt,'',$DelPos,$DelLen); // delete the current locator + if ($Loc->AttForward) { + $Loc->AttTagBeg += -$DelLen; + $Loc->AttTagEnd += -$DelLen; + } elseif ($Loc->PosBeg<$Loc->AttTagEnd) { + $Loc->AttTagEnd += -$DelLen; + } + + $InsPos = false; + if ($Loc->AttBeg===false) { + $InsPos = $Loc->AttTagEnd; + if ($Txt[$InsPos-1]==='/') $InsPos--; + if ($Txt[$InsPos-1]===' ') $InsPos--; + $Ins1 = ' '.$Loc->AttName.'='.$AttDelim; + $Ins2 = $AttDelim; + $Loc->AttBeg = $InsPos + 1; + $Loc->AttValBeg = $InsPos + strlen($Ins1) - 1; + } else { + if ($Loc->PosEnd<$Loc->AttBeg) $Loc->AttBeg += -$DelLen; + if ($Loc->PosEnd<$Loc->AttEnd) $Loc->AttEnd += -$DelLen; + if ($Loc->AttValBeg===false) { + $InsPos = $Loc->AttEnd+1; + $Ins1 = '='.$AttDelim; + $Ins2 = $AttDelim; + $Loc->AttValBeg = $InsPos+1; + } elseif (isset($Loc->PrmLst['attadd'])) { + $InsPos = $Loc->AttEnd; + $Ins1 = ' '; + $Ins2 = ''; + } else { + // value already existing + if ($Loc->PosEnd<$Loc->AttValBeg) $Loc->AttValBeg += -$DelLen; + $PosBeg = $Loc->AttValBeg; + $PosEnd = $Loc->AttEnd; + if ($Loc->AttDelimCnt>0) {$PosBeg++; $PosEnd--;} + } + } + + if ($InsPos===false) { + $InsLen = 0; + } else { + $InsTxt = $Ins1.'[]'.$Ins2; + $InsLen = strlen($InsTxt); + $PosBeg = $InsPos + strlen($Ins1); + $PosEnd = $PosBeg + 1; + $Txt = substr_replace($Txt,$InsTxt,$InsPos,0); + $Loc->AttEnd = $InsPos + $InsLen - 1; + $Loc->AttTagEnd += $InsLen; + } + + $Loc->PosBeg = $PosBeg; + $Loc->PosEnd = $PosEnd; + + // for CacheField + if (is_array($MoveLocLst)) { + $Loc->InsPos = $InsPos; + $Loc->InsLen = $InsLen; + $Loc->DelPos = $DelPos; + if ($Loc->InsPos < $Loc->DelPos) $Loc->DelPos += $InsLen; + $Loc->DelLen = $DelLen; + self::f_Loc_Moving($Loc, $MoveLocLst); + } + + return true; + +} + +static function f_Xml_Max(&$Txt,&$Nbr,$MaxEnd) { +// Limit the number of HTML chars + + $pMax = strlen($Txt)-1; + $p=0; + $n=0; + $in = false; + $ok = true; + + while ($ok) { + if ($in) { + if ($Txt[$p]===';') { + $in = false; + $n++; + } + } else { + if ($Txt[$p]==='&') { + $in = true; + } else { + $n++; + } + } + if (($n>=$Nbr) || ($p>=$pMax)) { + $ok = false; + } else { + $p++; + } + } + + if (($n>=$Nbr) && ($p<$pMax)) $Txt = substr($Txt,0,$p).$MaxEnd; + +} + +static function f_Xml_GetPart(&$Txt, $TagLst, $AllIfNothing=false) { +// Returns parts of the XML/HTML content, default is BODY. + + if (($TagLst===true) || ($TagLst==='')) $TagLst = 'body'; + + $x = ''; + $nothing = true; + $TagLst = explode('+',$TagLst); + + // Build a clean list of tags + foreach ($TagLst as $i=>$t) { + if ((substr($t,0,1)=='(') && (substr($t,-1,1)==')')) { + $t = substr($t,1,strlen($t)-2); + $Keep = true; + } else { + $Keep = false; + } + $TagLst[$i] = array('t'=>$t, 'k'=>$Keep, 'b'=>-1, 'e'=>-1, 's'=>false); + } + + $PosOut = strlen($Txt); + $Pos = 0; + + // Optimized search for all tag types + do { + + // Search next positions of each tag type + $TagMin = false; // idx of the tag at first position + $PosMin = $PosOut; // pos of the tag at first position + foreach ($TagLst as $i=>$Tag) { + if ($Tag['b']<$Pos) { + $Loc = self::f_Xml_FindTag($Txt,$Tag['t'],true,$Pos,true,false,false); + if ($Loc===false) { + $Tag['b'] = $PosOut; // tag not found, no more search on this tag + } else { + $Tag['b'] = $Loc->PosBeg; + $Tag['e'] = $Loc->PosEnd; + $Tag['s'] = (substr($Txt,$Loc->PosEnd-1,1)==='/'); // true if it's a single tag + } + $TagLst[$i] = $Tag; // update + } + if ($Tag['b']<$PosMin) { + $TagMin = $i; + $PosMin = $Tag['b']; + } + } + + // Add the part of tag types + if ($TagMin!==false) { + $Tag = &$TagLst[$TagMin]; + $Pos = $Tag['e']+1; + if ($Tag['s']) { + // single tag + if ($Tag['k']) $x .= substr($Txt,$Tag['b'] ,$Tag['e'] - $Tag['b'] + 1); + } else { + // search the closing tag + $Loc = self::f_Xml_FindTag($Txt,$Tag['t'],false,$Pos,true,false,false); + if ($Loc===false) { + $Tag['b'] = $PosOut; // closing tag not found, no more search on this tag + } else { + $nothing = false; + if ($Tag['k']) { + $x .= substr($Txt,$Tag['b'] ,$Loc->PosEnd - $Tag['b'] + 1); + } else { + $x .= substr($Txt,$Tag['e']+1,$Loc->PosBeg - $Tag['e'] - 1); + } + $Pos = $Loc->PosEnd + 1; + } + } + } + + } while ($TagMin!==false); + + if ($AllIfNothing && $nothing) return $Txt; + return $x; + +} + +/** + * Find the start position of an XML tag. Used by OpenTBS. + * $Case=false can be useful for HTML. + * $Tag='' should work and found the start of the first opening tag of any name. + * $Tag='/' should work and found the start of the first closing tag of any name. + * Encapsulation levels are not featured yet. + */ +static function f_Xml_FindTagStart(&$Txt,$Tag,$Opening,$PosBeg,$Forward,$Case=true) { + + if ($Txt==='') return false; + + $x = '<'.(($Opening) ? '' : '/').$Tag; + $xl = strlen($x); + + $p = $PosBeg - (($Forward) ? 1 : -1); + + if ($Case) { + do { + if ($Forward) $p = strpos($Txt,$x,$p+1); else $p = strrpos(substr($Txt,0,$p+1),$x); + if ($p===false) return false; + $z = substr($Txt,$p+$xl,1); + } while ( ($z!==' ') && ($z!=="\r") && ($z!=="\n") && ($z!=='>') && ($z!=='/') && ($Tag!=='/') && ($Tag!=='') ); + } else { + do { + if ($Forward) $p = stripos($Txt,$x,$p+1); else $p = strripos(substr($Txt,0,$p+1),$x); + if ($p===false) return false; + $z = substr($Txt,$p+$xl,1); + } while ( ($z!==' ') && ($z!=="\r") && ($z!=="\n") && ($z!=='>') && ($z!=='/') && ($Tag!=='/') && ($Tag!=='') ); + } + + return $p; + +} + +/** + * This function is a smart solution to find an XML tag. + * It allows to ignore full opening/closing couple of tags that could be inserted before the searched tag. + * It allows also to pass a number of encapsulations. + * To ignore encapsulation and opengin/closing just set $LevelStop=false. + * $Opening is used only when $LevelStop=false. + */ +static function f_Xml_FindTag(&$Txt,$Tag,$Opening,$PosBeg,$Forward,$LevelStop,$WithPrm,$WithPos=false) { + + if ($Tag==='_') { // New line + $p = self::f_Xml_FindNewLine($Txt,$PosBeg,$Forward,($LevelStop!==0)); + $Loc = new clsTbsLocator; + $Loc->PosBeg = ($Forward) ? $PosBeg : $p; + $Loc->PosEnd = ($Forward) ? $p : $PosBeg; + $Loc->RightLevel = 0; + return $Loc; + } + + $Pos = $PosBeg + (($Forward) ? -1 : +1); + $TagIsOpening = false; + $TagClosing = '/'.$Tag; + $LevelNum = 0; + $TagOk = false; + $PosEnd = false; + $TagL = strlen($Tag); + $TagClosingL = strlen($TagClosing); + $RightLevel = 0; + + do { + + // Look for the next tag def + if ($Forward) { + $Pos = strpos($Txt,'<',$Pos+1); + } else { + if ($Pos<=0) { + $Pos = false; + } else { + $Pos = strrpos(substr($Txt,0,$Pos - 1),'<'); // strrpos() syntax compatible with PHP 4 + } + } + + if ($Pos!==false) { + + // Check the name of the tag + if (strcasecmp(substr($Txt,$Pos+1,$TagL),$Tag)==0) { + // It's an opening tag + $PosX = $Pos + 1 + $TagL; // The next char + $TagOk = true; + $TagIsOpening = true; + } elseif (strcasecmp(substr($Txt,$Pos+1,$TagClosingL),$TagClosing)==0) { + // It's a closing tag + $PosX = $Pos + 1 + $TagClosingL; // The next char + $TagOk = true; + $TagIsOpening = false; + } + + if ($TagOk) { + // Check the next char + $x = $Txt[$PosX]; + if (($x===' ') || ($x==="\r") || ($x==="\n") || ($x==='>') || ($x==='/') || ($Tag==='/') || ($Tag==='')) { + // Check the encapsulation count + if ($LevelStop===false) { // No encapsulation check + if ($TagIsOpening!==$Opening) $TagOk = false; + } else { // Count the number of level + if ($TagIsOpening) { + $PosEnd = strpos($Txt,'>',$PosX); + if ($PosEnd!==false) { + if ($Txt[$PosEnd-1]==='/') { + if (($Pos<$PosBeg) && ($PosEnd>$PosBeg)) {$RightLevel=1; $LevelNum++;} + } else { + $LevelNum++; + } + } + } else { + $LevelNum--; + } + // Check if it's the expected level + if ($LevelNum!=$LevelStop) { + $TagOk = false; + $PosEnd = false; + } + } + } else { + $TagOk = false; + } + } //--> if ($TagOk) + + } + } while (($Pos!==false) && ($TagOk===false)); + + // Search for the end of the tag + if ($TagOk) { + $Loc = new clsTbsLocator; + if ($WithPrm) { + self::f_Loc_PrmRead($Txt,$PosX,true,'\'"','<','>',$Loc,$PosEnd,$WithPos); + } elseif ($PosEnd===false) { + $PosEnd = strpos($Txt,'>',$PosX); + if ($PosEnd===false) { + $TagOk = false; + } + } + } + + // Result + if ($TagOk) { + $Loc->PosBeg = $Pos; + $Loc->PosEnd = $PosEnd; + $Loc->RightLevel = $RightLevel; + return $Loc; + } else { + return false; + } + +} + +static function f_Xml_FindNewLine(&$Txt,$PosBeg,$Forward,$IsRef) { + + $p = $PosBeg; + if ($Forward) { + $Inc = 1; + $Inf = &$p; + $Sup = strlen($Txt)-1; + } else { + $Inc = -1; + $Inf = 0; + $Sup = &$p; + } + + do { + if ($Inf>$Sup) return max($Sup,0); + $x = $Txt[$p]; + if (($x==="\r") || ($x==="\n")) { + $x2 = ($x==="\n") ? "\r" : "\n"; + $p0 = $p; + if (($Inf<$Sup) && ($Txt[$p+$Inc]===$x2)) $p += $Inc; // Newline char can have two chars. + if ($Forward) return $p; // Forward => return pos including newline char. + if ($IsRef || ($p0!=$PosBeg)) return $p0+1; // Backwars => return pos without newline char. Ignore newline if it is the very first char of the search. + } + $p += $Inc; + } while (true); + +} + +static function f_Xml_GetNextEntityName($Txt, $Pos, &$tag, &$PosBeg, &$p) { +/* + $tag : tag name + $PosBeg : position of the tag + $p : position where the read has stop + $z : first char after the name +*/ + + $tag = ''; + $PosBeg = strpos($Txt, '<', $Pos); + + if ($PosBeg===false) return false; + + // Read the name of the tag + $go = true; + $p = $PosBeg; + while ($go) { + $p++; + $z = $Txt[$p]; + if ($go = ($z!==' ') && ($z!=="\r") && ($z!=="\n") && ($z!=='>') && ($z!=='/') ) { + $tag .= $z; + } + } + + return true; + +} + +} diff --git a/lib/tbs_plugin_opentbs.php b/lib/tbs_plugin_opentbs.php new file mode 100755 index 0000000..49dc242 --- /dev/null +++ b/lib/tbs_plugin_opentbs.php @@ -0,0 +1,9162 @@ +TBS; + + if (!isset($TBS->OtbsAutoLoad)) $TBS->OtbsAutoLoad = true; // TBS will load the subfile regarding to the extension of the archive + if (!isset($TBS->OtbsConvBr)) $TBS->OtbsConvBr = false; // string for NewLine conversion + if (!isset($TBS->OtbsAutoUncompress)) $TBS->OtbsAutoUncompress = $this->Meth8Ok; + if (!isset($TBS->OtbsConvertApostrophes)) $TBS->OtbsConvertApostrophes = true; + if (!isset($TBS->OtbsSpacePreserve)) $TBS->OtbsSpacePreserve = true; + if (!isset($TBS->OtbsClearWriter)) $TBS->OtbsClearWriter = true; + if (!isset($TBS->OtbsClearMsWord)) $TBS->OtbsClearMsWord = true; + if (!isset($TBS->OtbsMsExcelConsistent)) $TBS->OtbsMsExcelConsistent = true; + if (!isset($TBS->OtbsMsExcelExplicitRef)) $TBS->OtbsMsExcelExplicitRef = true; + if (!isset($TBS->OtbsClearMsPowerpoint)) $TBS->OtbsClearMsPowerpoint = true; + if (!isset($TBS->OtbsGarbageCollector)) $TBS->OtbsGarbageCollector = true; + if (!isset($TBS->OtbsMsExcelCompatibility)) $TBS->OtbsMsExcelCompatibility = true; + $this->Version = '1.12.1'; + $this->DebugLst = false; // deactivate the debug mode + $this->ExtInfo = false; + $TBS->TbsZip = &$this; // a shortcut + return array('BeforeLoadTemplate','BeforeShow', 'OnCommand', 'OnOperation', 'OnCacheField'); + } + + function BeforeLoadTemplate(&$File,&$Charset) { + + $TBS =& $this->TBS; + if ($TBS->_Mode!=0) return; // If we are in subtemplate mode, the we use the TBS default process + + if ($File === false) { + // Close the current template if any + @$this->Close(); + // Save memory space + $this->TbsInitArchive(); + return false; + } + + // Decompose the file path. The syntaxe is 'Archive.ext#subfile', or 'Archive.ext', or '#subfile' + $FilePath = $File; + $SubFileLst = false; + if (is_string($File)) { + $p = strpos($File, '#'); + if ($p!==false) { + $FilePath = substr($File,0,$p); + $SubFileLst = substr($File,$p+1); + } + } + + // Open the archive + if ($FilePath!=='') { + $ok = @$this->Open($FilePath); // Open the archive + if (!$ok) { + if ($this->ArchHnd===false) { + return $this->RaiseError("The template '".$this->ArchFile."' cannot be found."); + } else { + return false; + } + } + $this->TbsInitArchive(); // Initialize other archive informations + if ($TBS->OtbsAutoLoad && ($this->ExtInfo!==false) && ($SubFileLst===false)) { + // auto load files from the archive + $SubFileLst = $this->ExtInfo['load']; + $TBS->OtbsConvBr = $this->ExtInfo['br']; + } + $TBS->OtbsSubFileLst = $SubFileLst; + } elseif ($this->ArchFile==='') { + $this->RaiseError('Cannot read file(s) "'.$SubFileLst.'" because no archive is opened.'); + } + + // Change the Charset if a new archive is opended, or if LoadTemplate is called explicitely for that + if (($FilePath!=='') || ($File==='')) { + if ($Charset===OPENTBS_ALREADY_XML) { + $TBS->LoadTemplate('', false); // Define the function for string conversion + } elseif ($Charset===OPENTBS_ALREADY_UTF8) { + $TBS->LoadTemplate('', array(&$this,'ConvXmlOnly')); // Define the function for string conversion + } else { + $TBS->LoadTemplate('', array(&$this,'ConvXmlUtf8')); // Define the function for string conversion + } + } + + // Load the subfile(s) + if (($SubFileLst!=='') && ($SubFileLst!==false)) { + if (is_string($SubFileLst)) $SubFileLst = explode(';',$SubFileLst); + $this->TbsLoadSubFileAsTemplate($SubFileLst); + } + + if ($FilePath!=='') $TBS->_LastFile = $this->ArchFile; + + return false; // default LoadTemplate() process is not executed + + } + + function BeforeShow(&$Render, $File='') { + + $TBS =& $this->TBS; + + if ($this->ArchFile==='') { + return $this->RaiseError('Command Show() cannot be processed because no archive is opened.'); + } + + if ($TBS->_Mode!=0) return; // If we are in subtemplate mode, the we use the TBS default process + + if ($this->TbsSystemCredits) { + $this->Misc_EditCredits("OpenTBS " . $this->Version, true, true); + } + + $this->TbsStorePark(); // Save the current loaded subfile if any + + $TBS->Plugin(-4); // deactivate other plugins + + $Debug = (($Render & OPENTBS_DEBUG_XML)==OPENTBS_DEBUG_XML); + if ($Debug) $this->DebugLst = array(); + + $TbsShow = (($Render & OPENTBS_DEBUG_AVOIDAUTOFIELDS)!=OPENTBS_DEBUG_AVOIDAUTOFIELDS); + + switch ($this->ExtEquiv) { + case 'ods': $this->OpenDoc_SheetSlides_DeleteAndDisplay(true); break; + case 'odp': $this->OpenDoc_SheetSlides_DeleteAndDisplay(false); break; + case 'xlsx': $this->MsExcel_SheetDeleteAndDisplay(); break; + case 'pptx': $this->MsPowerpoint_SlideDelete(); break; + } + + $explicitRef = ($TBS->OtbsMsExcelExplicitRef && ($this->ExtEquiv==='xlsx')); + + // Merges all modified subfiles + $idx_lst = array_keys($this->TbsStoreLst); + foreach ($idx_lst as $idx) { + $TBS->Source = $this->TbsStoreLst[$idx]['src']; + $onshow = $this->TbsStoreLst[$idx]['onshow']; + unset($this->TbsStoreLst[$idx]); // save memory space + $TBS->OtbsCurrFile = $this->TbsGetFileName($idx); // usefull for TbsPicAdd() + $this->TbsCurrIdx = $idx; // usefull for debug mode + if ($TbsShow && $onshow) $TBS->Show(TBS_NOTHING); + if ($this->ExtEquiv == 'docx') { + $this->MsWord_RenumDocPr($TBS->Source); + } + if ($explicitRef && (!isset($this->MsExcel_KeepRelative[$idx])) ) { + $this->MsExcel_ConvertToExplicit($TBS->Source); + } + if ($Debug) $this->DebugLst[$this->TbsGetFileName($idx)] = $TBS->Source; + $this->FileReplace($idx, $TBS->Source, TBSZIP_STRING, $TBS->OtbsAutoUncompress); + } + $TBS->Plugin(-10); // reactivate other plugins + $this->TbsCurrIdx = false; + + if ($this->OpenXmlCTypes!==false) $this->OpenXML_CTypesCommit($Debug); // Commit special OpenXML features if any + if ($this->OpenDocManif!==false) $this->OpenDoc_ManifestCommit($Debug); // Commit special OpenDocument features if any + if ($this->OpenXmlRid!==false) $this->OpenXML_Rels_CommitNewRids($Debug); // Must be done also after the loop because some Rid can be added with [onshow] + + if ($TBS->OtbsGarbageCollector) { + if ($this->ExtType=='openxml') $this->OpenMXL_GarbageCollector(); + } + + if ( ($TBS->ErrCount>0) && (!$TBS->NoErr) && (!$Debug)) { + $TBS->meth_Misc_Alert('Show() Method', 'The output is cancelled by the OpenTBS plugin because at least one error has occured.'); + exit; + } + + if ($Debug) { + // Do the debug even if other options are used + $this->TbsDebug_Merge(true, false); + } elseif (($Render & TBS_OUTPUT)==TBS_OUTPUT) { // notice that TBS_OUTPUT = OPENTBS_DOWNLOAD + // download + $ContentType = (isset($this->ExtInfo['ctype'])) ? $this->ExtInfo['ctype'] : ''; + $this->Flush($Render, $File, $ContentType); // $Render is used because it can contain options OPENTBS_DOWNLOAD and OPENTBS_NOHEADER. + $Render = $Render - TBS_OUTPUT; //prevent TBS from an extra output. + } elseif(($Render & OPENTBS_FILE)==OPENTBS_FILE) { + // to file + $this->Flush(TBSZIP_FILE, $File); + } elseif(($Render & OPENTBS_STRING)==OPENTBS_STRING) { + // to string + $this->Flush(TBSZIP_STRING); + $TBS->Source = $this->OutputSrc; + $this->OutputSrc = ''; + } + + if (($Render & TBS_EXIT)==TBS_EXIT) { + $this->Close(); + exit; + } + + return false; // cancel the default Show() process + + } + + function OnCacheField($BlockName,&$Loc,&$Txt,$PrmProc) { + + if (isset($Loc->PrmLst['ope'])) { + + $ope_lst = explode(',', $Loc->PrmLst['ope']); // in this event, ope is not exploded + + // Prepare to change picture + if (in_array('changepic', $ope_lst)) { + $this->TbsPicPrepare($Txt, $Loc, true); // add parameter "att" which will be processed just after this event, when the field is cached + } elseif (in_array('mergecell', $ope_lst)) { + $this->TbsPrepareMergeCell($Txt, $Loc); + } elseif (in_array('docfield', $ope_lst)) { + $this->TbsDocFieldPrepare($Txt, $Loc); + } + + // Change cell type + foreach($ope_lst as $ope) { + $x = substr($ope,0,4); + if( ($x==='tbs:') || ($x==='xlsx') || (substr($ope,0,3)==='ods') ) { + if ($this->ExtEquiv==='ods') { + $z = ''; + $this->OpenDoc_ChangeCellType($Txt, $Loc, $ope, false, $z); + } elseif ($this->ExtEquiv==='xlsx') { + $this->MsExcel_ChangeCellType($Txt, $Loc, $ope); + } + return; // do only one change + } + } + + } + + } + + /** + * In this TBS event, parameter ope is exploded, and there is one function call for each ope command. + */ + function OnOperation($FieldName,&$Value,&$PrmLst,&$Txt,$PosBeg,$PosEnd,&$Loc) { + + $ope = $PrmLst['ope']; + if ($ope==='addpic') { + // for compatibility + $this->TbsPicAdd($Value, $PrmLst, $Txt, $Loc, 'ope=addpic'); + } elseif ($ope==='changepic') { + $this->TbsPicPrepare($Txt, $Loc, false); + $this->TbsPicAdd($Value, $PrmLst, $Txt, $Loc, 'ope=changepic'); + } elseif ($ope==='delcol') { + // Delete the TBS field otherwise "return false" will produce a TBS error "doesn't have any subname" with [onload] fields. + $Txt = substr_replace($Txt, '', $PosBeg, $PosEnd - $PosBeg + 1); + $this->TbsDeleteColumns($Txt, $Value, $PrmLst, $PosBeg); + return false; // prevent TBS from actually merging the field + } elseif ($ope==='mergecell') { + if (isset($this->PrevVals[$Loc->FullName])) { + if ($Value==$this->PrevVals[$Loc->FullName]) { + $Value = ''; + } else { + $this->PrevVals[$Loc->FullName] = $Value; + $Value = ''; + } + } + } elseif ($ope==='docfield') { + $this->TbsDocFieldPrepare($Txt, $Loc); + } else { + $x = substr($ope,0,4); + if( ($x==='tbs:') || ($x==='xlsx') || (substr($ope,0,3)==='ods') ) { + if ($this->ExtEquiv==='ods') { + if (!isset($Loc->PrmLst['cellok'])) $this->OpenDoc_ChangeCellType($Txt, $Loc, $ope, true, $Value); + } elseif ($this->ExtEquiv==='xlsx') { + if (!isset($Loc->PrmLst['cellok'])) $this->MsExcel_ChangeCellType($Txt, $Loc, $ope); + $this->MsExcel_ChangeCellValue($Loc, $Value); + } + } + } + } + + function OnCommand($Cmd, $x1=null, $x2=null, $x3=null, $x4=null, $x5=null) { + + if ($Cmd==OPENTBS_INFO) { + // Display debug information + echo "OpenTBS plugin Information
\r\n"; + return $this->Debug(); + } elseif ( ($Cmd==OPENTBS_DEBUG_INFO) || ($Cmd==OPENTBS_DEBUG_CHART_LIST) ) { + if (is_null($x1)) $x1 = true; + $this->TbsDebug_Info($x1); + return true; + } + + if($Cmd==OPENTBS_MAKE_OPTIMIZED_TEMPLATE) { + + // save options + $s_onload = $this->TBS->GetOption('onload'); + $s_onshow = $this->TBS->GetOption('onshow'); + + // change options + $this->TBS->SetOption('onload', false); + $this->TBS->SetOption('onshow', false); + + // load the template + $this->TBS->LoadTemplate($x1); + + if ($this->ExtEquiv == 'xlsx') { + // load all sheets + $this->MsExcel_SheetInit(); + foreach($this->MsExcel_Sheets as $o) { + $this->TbsLoadSubFileAsTemplate('xl/'.$o->file); + } + } elseif ($this->ExtEquiv == 'pptx') { + // load all slides + $this->MsPowerpoint_InitSlideLst(); + foreach ($this->OpenXmlSlideLst as $s) { + $this->TbsLoadSubFileAsTemplate($s['file']); + } + } + + // save the result + $this->TBS->Show(OPENTBS_FILE + OPENTBS_DEBUG_AVOIDAUTOFIELDS, $x2); + + // restore options + $this->TBS->SetOption('onload', $s_onload); + $this->TBS->SetOption('onshow', $s_onshow); + + return true; + + } + + // Check that a template is loaded + if ($this->ExtInfo===false) { + $this->RaiseError("Cannot execute the plug-in commande because no template is loaded."); + return true; + } + + if ($Cmd==OPENTBS_RESET) { + + // Reset all mergings + $this->ArchCancelModif(); + $this->TbsStoreLst = array(); + $TBS =& $this->TBS; + $TBS->Source = ''; + $TBS->OtbsCurrFile = false; + if (is_string($TBS->OtbsSubFileLst)) { + $f = '#'.$TBS->OtbsSubFileLst; + $h = ''; + $this->BeforeLoadTemplate($f,$h); + } + return true; + + } elseif ($Cmd==OPENTBS_SELECT_FILE) { + + // Raise an error is the file is not found + return $this->TbsLoadSubFileAsTemplate($x1); + + } elseif ( ($Cmd==OPENTBS_ADDFILE) || ($Cmd==OPENTBS_REPLACEFILE) ) { + + // Add a new file or cancel a previous add + $Name = (is_null($x1)) ? false : $x1; + $Data = (is_null($x2)) ? false : $x2; + $DataType = (is_null($x3)) ? TBSZIP_STRING : $x3; + $Compress = (is_null($x4)) ? true : $x4; + + if ($Cmd==OPENTBS_ADDFILE) { + return $this->FileAdd($Name, $Data, $DataType, $Compress); + } else { + return $this->FileReplace($Name, $Data, $DataType, $Compress); + } + + } elseif ($Cmd==OPENTBS_DELETEFILE) { + + // Delete an existing file in the archive + $Name = (is_null($x1)) ? false : $x1; + $this->FileCancelModif($Name, false); // cancel added files + return $this->FileReplace($Name, false); // mark the file as to be deleted + + } elseif ($Cmd==OPENTBS_FILEEXISTS) { + + return $this->FileExists($x1); + + } elseif ($Cmd==OPENTBS_CHART) { + + $ChartRef = $x1; + $SeriesNameOrNum = $x2; + $NewValues = (is_null($x3)) ? false : $x3; + $NewLegend = (is_null($x4)) ? false : $x4; + + if ($this->ExtType=='odf') { + return $this->OpenDoc_ChartChangeSeries($ChartRef, $SeriesNameOrNum, $NewValues, $NewLegend); + } else { + return $this->OpenXML_ChartChangeSeries($ChartRef, $SeriesNameOrNum, $NewValues, $NewLegend); + } + } elseif ($Cmd==OPENTBS_CHART_INFO) { + + $ChartRef = $x1; + $Complete = $x2; + + if ($this->ExtType=='odf') { + return $this->OpenDoc_ChartReadSeries($ChartRef, $Complete); + } else { + return $this->OpenXML_ChartReadSeries($ChartRef, $Complete); + } + + + } elseif ($Cmd==OPENTBS_DEBUG_XML_SHOW) { + + $this->TBS->Show(OPENTBS_DEBUG_XML); + + } elseif ($Cmd==OPENTBS_DEBUG_XML_CURRENT) { + + $this->TbsStorePark(); + $this->DebugLst = array(); + foreach ($this->TbsStoreLst as $idx=>$park) $this->DebugLst[$this->TbsGetFileName($idx)] = $park['src']; + $this->TbsDebug_Merge(true, true); + + if (is_null($x1) || $x1) exit(); + + } elseif($Cmd==OPENTBS_FORCE_DOCTYPE) { + + return $this->Ext_PrepareInfo($x1); + + } elseif ($Cmd==OPENTBS_DELETE_ELEMENTS) { + + if (is_string($x1)) $x1 = explode(',', $x1); + if (is_null($x2)) $x2 = false; // OnlyInner + return $this->XML_DeleteElements($this->TBS->Source, $x1, $x2); + + } elseif ($Cmd==OPENTBS_SELECT_MAIN) { + + if ( ($this->ExtInfo!==false) && isset($this->ExtInfo['main']) ) { + $this->TbsLoadSubFileAsTemplate($this->ExtInfo['main']); + return true; + } else { + return false; + } + + } elseif ($Cmd==OPENTBS_SELECT_SHEET) { + + if ($this->ExtEquiv=='ods') { + $this->TbsLoadSubFileAsTemplate($this->ExtInfo['main']); + return true; + } + + // Only XLSX files have sheets in separated subfiles. + if ($this->ExtEquiv==='xlsx') { + $SearchBy = ($x2) ? array('name', 'sheetId') : array('name', 'num'); + $o = $this->MsExcel_SheetGetConf($x1, $SearchBy, true); + if ($o===false) return false; + if ($o->file===false) return $this->RaiseError("($Cmd) Error with sheet '$x1'. The corresponding XML subfile is not referenced."); + return $this->TbsLoadSubFileAsTemplate('xl/'.$o->file); + } + + return false; + + } elseif ( ($Cmd==OPENTBS_DELETE_SHEETS) || ($Cmd==OPENTBS_DISPLAY_SHEETS) || ($Cmd==OPENTBS_DELETE_SLIDES) || ($Cmd==OPENTBS_DISPLAY_SLIDES) ) { + + $delete = ( ($Cmd==OPENTBS_DELETE_SHEETS) || ($Cmd==OPENTBS_DELETE_SLIDES) ) ; + $this->TbsSheetSlide_DeleteDisplay($x1, $x2, $delete); + + } elseif ($Cmd==OPENTBS_MERGE_SPECIAL_ITEMS) { + + if ($this->ExtEquiv!='xlsx') return 0; + $lst = $this->MsExcel_GetDrawingLst(); + $this->TbsQuickLoad($lst); + + } elseif ($Cmd==OPENTBS_SELECT_SLIDE) { + + $master = (is_null($x2)) ? false : $x2; + + if ($this->ExtEquiv=='odp') { + // ODP + // All slides are in the same sub-file + $file = ($master) ? 'styles.xml' : $this->ExtInfo['main']; + $this->TbsLoadSubFileAsTemplate($file); + return true; + } + + if ($this->ExtEquiv!='pptx') return false; + + // PPTX + + $slide = ($master) ? 'slide master' : 'slide'; + $RefLst = $this->MsPowerpoint_InitSlideLst($master); + + $s = intval($x1)-1; + if (isset($RefLst[$s])) { + $this->TbsLoadSubFileAsTemplate($RefLst[$s]['idx']); + return true; + } else { + return $this->RaiseError("($Cmd) $slide number $x1 is not found inside the Presentation."); + } + + } elseif ($Cmd==OPENTBS_DELETE_COMMENTS) { + + // Default values + $MainTags = false; + $CommFiles = false; + $CommTags = false; + $Inner = false; + + if ($this->ExtType=='odf') { + $MainTags = array('office:annotation', 'officeooo:annotation'); // officeooo:annotation is used in ODP Presentations + } else { + switch ($this->ExtEquiv) { + case 'docx': + $MainTags = array('w:commentRangeStart', 'w:commentRangeEnd', 'w:commentReference'); + $CommFiles = array('wordprocessingml.comments+xml'); + $CommTags = array('w:comment'); + $Inner = true; + break; + case 'xlsx': + $CommFiles = array('spreadsheetml.comments+xml'); + $CommTags = array('comment', 'author'); + break; + case 'pptx': + $CommFiles = array('presentationml.comments+xml'); + $CommTags = array('p:cm'); + break; + default: + return 0; + } + } + + return $this->TbsDeleteComments($MainTags, $CommFiles, $CommTags, $Inner); + + } elseif ($Cmd==OPENTBS_CHANGE_PICTURE) { + + static $UniqueId = 0; + + $code = $x1; + $file = $x2; + $prms = array('default'=>'current', 'adjust' => 'inside'); + if (is_array($x3)) { + $prms = array_merge($prms, $x3); + } else { + // Compatibility v <= 1.9.0 + if (!is_null($x3)) $prms['default'] = $x3; + if (!is_null($x4)) $prms['adjust'] = $x4; + } + $prms_flat = array(); + foreach($prms as $p => $v) $prms_flat[] = $p.'='.$v; + $prms_flat = implode(';', $prms_flat); + + $UniqueId++; + $name = 'OpenTBS_Change_Picture_'.$UniqueId; + $tag = "[$name;ope=changepic;tagpos=inside;$prms_flat]"; + + $nbr = false; + $TBS =& $this->TBS; + $TBS->Source = str_replace($code, $tag, $TBS->Source, $nbr); // argument $nbr supported buy PHP >= 5 + if ($nbr!==0) $TBS->MergeField($name, $file); + + return $nbr; + + } elseif ($Cmd==OPENTBS_COUNT_SLIDES) { + + $master = (is_null($x1)) ? false : $x1; + + if ($this->ExtEquiv=='pptx') { + $RefLst = $this->MsPowerpoint_InitSlideLst($master); + return count($RefLst); + } elseif ($this->ExtEquiv=='odp') { + $idx = $this->Ext_GetMainIdx(); + $txt = $this->TbsStoreGet($idx, "Command OPENTBS_COUNT_SLIDES"); + return substr_count($txt, ''); + } else { + return 0; + } + + } elseif ($Cmd==OPENTBS_COUNT_SHEETS) { + + if ($this->ExtEquiv=='xlsx') { + $this->MsExcel_SheetInit(); + return count($this->MsExcel_Sheets); + } elseif ($this->ExtEquiv=='ods') { + $idx = $this->Ext_GetMainIdx(); + $txt = $this->TbsStoreGet($idx, "Command OPENTBS_COUNT_SHEETS"); + return substr_count($txt, ''); + } else { + return 0; + } + + } elseif ($Cmd==OPENTBS_SEARCH_IN_SLIDES) { + + if ($this->ExtEquiv=='pptx') { + $option = (is_null($x2)) ? OPENTBS_FIRST : $x2; + $returnFirstFound = (($option & OPENTBS_ALL)!=OPENTBS_ALL); + $find = $this->MsPowerpoint_SearchInSlides($x1, true, $returnFirstFound); + if ($returnFirstFound) { + $slide = $find['key']; + if ( ($slide!==false) && (($option & OPENTBS_GO)==OPENTBS_GO) ) $this->OnCommand(OPENTBS_SELECT_SLIDE, $slide); + return ($slide); + } else { + $res = array(); + foreach($find as $f) $res[] = $f['key']; + return $res; + } + } elseif ($this->ExtEquiv=='odp') { + // Only for compatibility + $p = strpos($this->TBS->Source, $x1); + return ($p===false) ? false : 1; + } else { + return false; + } + + } elseif ( ($Cmd==OPENTBS_SELECT_HEADER) || ($Cmd==OPENTBS_SELECT_FOOTER) ) { + + $file = false; + + switch ($this->ExtEquiv) { + case 'docx': + $x2 = intval($x2); // 0 by default + $file = $this->MsWord_GetHeaderFooterFile($Cmd, $x1, $x2); + break; + case 'odt': + $file = 'styles.xml'; + break; + case 'ods': case 'odp': + $this->ExtInfo['main']; + break; + case 'xlsx': case 'pptx': + return false; + break; + } + + return $this->TbsLoadSubFileAsTemplate($file); + + } elseif ($Cmd==OPENTBS_GET_HEADERS_FOOTERS) { + + $res = array(); + + switch ($this->ExtEquiv) { + case 'docx': + $this->MsWord_InitHeaderFooter(); + foreach ($this->MsWord_HeaderFooter as $info) { + $res[] = $info['file']; + } + break; + case 'odt': + $res[] = 'styles.xml'; + break; + case 'ods': case 'odp': + // Headers and footers are in the main file. + // Handout headers and footers for presentations (PPTX & ODP) are not supported for now. + if (isset($this->ExtInfo['main'])) $res[] = $this->ExtInfo['main']; + break; + case 'xlsx': + $FileName = $this->CdFileLst[$this->TbsCurrIdx]; + if ($this->MsExcel_SheetIsIt($FileName) ) $res[] = $FileName; + break; + case 'pptx': + // Headers and footers are in the selected sheet or slide. + $FileName = $this->CdFileLst[$this->TbsCurrIdx]; + if ($this->MsPowerpoint_SlideIsIt($FileName) ) $res[] = $FileName; + break; + } + + return $res; + + } elseif ($Cmd==OPENTBS_SYSTEM_CREDIT) { + + $x1 = (boolean) $x1; + $this->TbsSystemCredits = $x1; + return $x1; + + } elseif ($Cmd==OPENTBS_ADD_CREDIT) { + + return $this->Misc_EditCredits($x1, true, false, $x2); + + } elseif ($Cmd==OPENTBS_RELATIVE_CELLS) { + + $KeepRelative = (boolean) $x1; + if ($x2 == OPENTBS_ALL) { + // Al$ sheets + $this->TBS->OtbsMsExcelExplicitRef = (!$KeepRelative); + } else { + // Current sheet + if ($KeepRelative) { + $this->MsExcel_KeepRelative[$this->TbsCurrIdx] = true; + } else { + unset($this->MsExcel_KeepRelative[$this->TbsCurrIdx]); + } + } + return $KeepRelative; + + } elseif ($Cmd==OPENTBS_EDIT_ENTITY) { + + $AddElIfMissing = (boolean) $x5; + return $this->XML_ReadWriteAtt($x1, $x2, $x3, $x4, $AddElIfMissing); + + } elseif ($Cmd==OPENTBS_READ_ENTITY) { + + return $this->XML_ReadWriteAtt($x1, $x2, $x3, null, false); + + } elseif ($Cmd==OPENTBS_GET_FILES) { + + $files = array(); + + // All files in the archive + foreach ($this->CdFileLst as $f) { + $files[] = $f['v_name']; + } + + return $files; + + } elseif ($Cmd==OPENTBS_GET_FILES_BY_TYPE) { + + $files = array(); + $types = $x1; + if (is_string($types)) { + $types = array($types); + } + + if ($this->ExtType=='odf') { + + // this commande is not really supported for LibreOffice + if (in_array('main', $types)) { + $files[] = $this->ExtInfo['main']; + } + + } elseif ($this->ExtType=='openxml') { + + // We convert alias into short types + $alias = array( + 'main' => array('wordprocessingml.document.main+xml'), + 'header' => array('wordprocessingml.header+xml'), + 'footer' => array('wordprocessingml.footer+xml'), + 'chart' => array('drawingml.chart+xml'), + 'slide' => array('presentationml.slide+xml'), + 'slidem' => array('presentationml.slideMaster+xml'), + 'sheet' => array('spreadsheetml.worksheet+xml'), + 'comments' => array('presentationml.notesSlide+xml', 'wordprocessingml.comments+xml', 'spreadsheetml.comments+xml'), + ); + + $types_conv = array(); + if (in_array('all', $types)) { + $types = array_merge($types, array_keys($alias)); + } + foreach ($types as $t) { + if ($t == 'all') { + } elseif (isset($alias[$t])) { + $types_conv = array_merge($types_conv, $alias[$t]); + } else { + $types_conv[] = $t; + } + } + + $files = $this->OpenXML_MapGetFiles($types_conv); + + } + + return $files; + + } elseif ($Cmd==OPENTBS_CHART_DELETE_CATEGORY) { + + if (is_null($x3)) { + $x3 = false; + } + + if ($this->ExtType=='odf') { + return $this->OpenDoc_ChartDelCategories($x1, $x2, $x3); + } elseif ($this->ExtType=='openxml') { + return $this->OpenXML_ChartDelCategories($x1, $x2, $x3); + } else { + return false; + } + + } elseif ($Cmd==OPENTBS_GET_OPENED_FILES) { + + $files = array(); + foreach ($this->TbsStoreLst as $idx => $info) { + // Files loaded manually, that are not the current selected file + if ($info['onshow'] && ($idx !== $this->TbsCurrIdx)) { + $name = $this->CdFileLst[$idx]['v_name']; + $files[] = $name; + } + } + // the current selected file + if ($this->TbsCurrIdx !== false) { + $name = $this->CdFileLst[$this->TbsCurrIdx]['v_name']; + $files[] = $name; + } + return $files; + + } elseif ($Cmd==OPENTBS_WALK_OPENED_FILES) { + + foreach ($this->TbsStoreLst as $idx => $info) { + // Files loaded manually, that are not the current selected file + if ($info['onshow'] && ($idx !== $this->TbsCurrIdx)) { + $name = $this->CdFileLst[$idx]['v_name']; + call_user_func_array($x1, array(&$this->TbsStoreLst[$idx]['src'], $name)); + } + } + // the current selected file + if ($this->TbsCurrIdx !== false) { + $name = $this->CdFileLst[$this->TbsCurrIdx]['v_name']; + call_user_func_array($x1, array(&$this->TBS->Source, $name)); + } + + } elseif ($Cmd == OPENTBS_GET_CELLS) { + + return $this->Sheet_VisitCells($x1, $x2, false); + + } elseif ($Cmd == OPENTBS_SET_CELLS) { + + return $this->RaiseError("Command OPENTBS_SET_CELLS will be available in a next version."); + } + + } + + // Initialize template information + function TbsInitArchive() { + + $TBS =& $this->TBS; + + $TBS->OtbsCurrFile = false; + + $this->TbsStoreLst = array(); + $this->TbsCurrIdx = false; + $this->TbsSystemCredits = true; + $this->TbsNoField = array(); // idx of sub-file having no TBS fields + $this->IdxToCheck = array(); // index of files to check + $this->PrevVals = array(); // Previous values for 'mergecell' operator + + $this->ImageIndex = 1; // Serial for inserted images + $this->ImageInternal = array(); // Internal names of inserted image + + $this->ExtEquiv = false; + $this->ExtType = false; + + // Common + $this->OtbsSheetSlidesDelete = array(); + $this->OtbsSheetSlidesVisible = array(); + $this->OtbsSheetRangeNames = false; + + // LibreOffice + $this->OpenDocCharts = false; + $this->OpenDocManif = false; + $this->OpenDoc_SheetSlides = false; + $this->OpenDoc_Styles = false; + + // MsOffice + $this->OpenXmlRid = false; + $this->OpenXmlCTypes = false; + $this->OpenXmlCharts = false; + $this->OpenXmlSharedStr = false; + $this->OpenXmlSlideLst = false; + $this->OpenXmlSlideMasterLst = false; + $this->MsExcel_Sheets = false; + $this->MsExcel_NoTBS = array(); // shared string containing no TBS field + $this->MsExcel_KeepRelative = array(); + $this->MsExcel_Formulas = array(); + $this->MsWord_HeaderFooter = false; + $this->MsWord_DocPrId = 0; + + $this->Ext_PrepareInfo(); // Set extension information + + } + + /** + * Load one or several sub-files of the archive as the current template. + * If a sub-template is loaded for the first time, then automatic merges and clean-up are performed. + * Return true if the file is correctly loaded. + * @param $SubFileLst Can be an index or a name or a file, or an array of such values. + */ + function TbsLoadSubFileAsTemplate($SubFileLst) { + + if (!is_array($SubFileLst)) $SubFileLst = array($SubFileLst); + + $ok = true; + $TBS = false; + + foreach ($SubFileLst as $SubFile) { + + $idx = $this->FileGetIdx($SubFile); + if ($idx===false) { + $ok = $this->RaiseError('Cannot load "'.$SubFile.'". The file is not found in the archive "'.$this->ArchFile.'".'); + } elseif ($idx!==$this->TbsCurrIdx) { + // Save the current loaded subfile if any + $this->TbsStorePark(); + // Load the subfile + if (!is_string($SubFile)) $SubFile = $this->TbsGetFileName($idx); + $this->TbsStoreLoad($idx, $SubFile); + if ($this->LastReadNotStored) { + // Loaded for the first time + if ($TBS===false) { + $this->TbsSwitchMode(true); // Configuration which prevents from other plug-ins when calling LoadTemplate() + $MergeAutoFields = $this->TbsMergeAutoFields(); + $TBS =& $this->TBS; + } + if ($this->LastReadComp<=0) { // the contents is not compressed + if ($this->ExtInfo!==false) { + $i = $this->ExtInfo; + $e = $this->ExtEquiv; + if ($this->TbsApplyOptim($TBS->Source, true)) { + if (isset($i['rpl_what'])) { + // auto replace strings in the loaded file + $TBS->Source = str_replace($i['rpl_what'], $i['rpl_with'], $TBS->Source); + } + if (($e==='odt') && $TBS->OtbsClearWriter) { + $this->OpenDoc_CleanRsID($TBS->Source); + } + if (($e==='ods') && $TBS->OtbsMsExcelCompatibility) { + $this->OpenDoc_DeleteUselessRepeatedElements($TBS->Source); + } + if ($e==='docx') { + if ($TBS->OtbsSpacePreserve) $this->MsWord_CleanSpacePreserve($TBS->Source); + if ($TBS->OtbsClearMsWord) $this->MsWord_Clean($TBS->Source); + } + if (($e==='pptx') && $TBS->OtbsClearMsPowerpoint) { + $this->MsPowerpoint_Clean($TBS->Source); + } + if (($e==='xlsx') && $TBS->OtbsMsExcelConsistent) { + $this->MsExcel_DeleteFormulaResults($idx, $TBS->Source); + $this->MsExcel_ConvertToRelative($TBS->Source); + } + } + } + // apply default TBS behaviors on the uncompressed content: other plug-ins + [onload] fields + if ($MergeAutoFields) $TBS->LoadTemplate(null,'+'); + } + } + } + + } + + if ($TBS!==false) $this->TbsSwitchMode(false); // Reactivate default configuration + + return $ok; + + } + + // Return true if automatic fields must be merged + function TbsMergeAutoFields() { + return (($this->TBS->Render & OPENTBS_DEBUG_AVOIDAUTOFIELDS)!=OPENTBS_DEBUG_AVOIDAUTOFIELDS); + } + + function TbsSwitchMode($PluginMode) { + $TBS = &$this->TBS; + if ($PluginMode) { + $this->_ModeSave = $TBS->_Mode; + $TBS->_Mode++; // deactivate TplVars[] reset and Charset reset. + $TBS->Plugin(-4); // deactivate other plugins + } else { + // Reactivate default configuration + $TBS->_Mode = $this->_ModeSave; + $TBS->Plugin(-10); // reactivate other plugins + } + } + + // Save the last opened subfile into the store, and close the subfile + function TbsStorePark() { + if ($this->TbsCurrIdx!==false) { + $this->TbsStoreLst[$this->TbsCurrIdx] = array('src'=>$this->TBS->Source, 'onshow'=>true); + $this->TBS->Source = ''; + $this->TbsCurrIdx = false; + } + } + + // Load a subfile from the store to be the current subfile + function TbsStoreLoad($idx, $file=false) { + $this->TBS->Source = $this->TbsStoreGet($idx, false); + $this->TbsCurrIdx = $idx; + if ($file===false) $file = $this->TbsGetFileName($idx); + $this->TBS->OtbsCurrFile = $file; + } + + /** + * Save a given source in the store. + * + * @param integer $idx Index of the sub-file. + * @param string $src New contents. + * @param boolean|null $onshow (optional, default is null) Null means unchanged, of false for new files. + * true means that TBS->Show() will be processed for the sub-file before the output. + * true aslo means the the sub-file will be considered as opened for the commands that return opened files. + */ + function TbsStorePut($idx, $src, $onshow = null) { + if ($idx===$this->TbsCurrIdx) { + $this->TBS->Source = $src; + } else { + if (is_null($onshow)) { + if (isset($this->TbsStoreLst[$idx])) { + $onshow = $this->TbsStoreLst[$idx]['onshow']; + } else { + $onshow = false; + } + } + $this->TbsStoreLst[$idx] = array('src'=>$src, 'onshow'=>$onshow); + } + } + + /** + * Return a source from the current merging, the store, or the archive. + * Take care that if the source it taken from the archive, then it is not saved in the store. + * @param {integer} $idx The index of the file to read. + * @param {string|false} $caller A text describing the calling function, for error reporting purpose. If caller=false it means TbsStoreLoad(). + */ + function TbsStoreGet($idx, $caller) { + $this->LastReadNotStored = false; + if ($idx===$this->TbsCurrIdx) { + return $this->TBS->Source; + } elseif (isset($this->TbsStoreLst[$idx])) { + $txt = $this->TbsStoreLst[$idx]['src']; + if ($caller===false) $this->TbsStoreLst[$idx]['src'] = ''; // save memory space + return $txt; + } else { + $this->LastReadNotStored = true; + $txt = $this->FileRead($idx, true); + if ($this->LastReadComp>0) { + if ($caller===false) { + return $txt; // return the uncompressed contents + } else { + return $this->RaiseError("(".$caller.") unable to uncompress '".$this->TbsGetFileName($idx)."'."); + } + } else { + return $txt; + } + } + } + + // Load a list of sub-files, but only if they have TBS fields. + // This is in order to merge automatic fields in special XML sub-files that are not usually loaded manually. + function TbsQuickLoad($NameLst) { + + if (!is_array($NameLst)) $NameLst = array($NameLst); + $nbr = 0; + $TBS = &$this->TBS; + + foreach ($NameLst as $FileName) { + $idx = $this->FileGetIdx($FileName); + if ( (!isset($this->TbsStoreLst[$idx])) && (!isset($this->TbsNoField[$idx])) ) { + $txt = $this->FileRead($idx, true); + if (strpos($txt, $TBS->_ChrOpen)!==false) { + // merge + $nbr++; + if ($nbr==1) { + $MergeAutoFields = $this->TbsMergeAutoFields(); + $SaveIdx = $this->TbsCurrIdx; // save the index of sub-file before the QuickLoad + $SaveName = $TBS->OtbsCurrFile; + $this->TbsSwitchMode(true); + } + $this->TbsStorePark(); // save the current file in the store + $TBS->Source = $txt; + unset($txt); + $TBS->OtbsCurrFile = $FileName; // may be needed for [onload] parameters + $this->TbsCurrIdx = $idx; + if ($MergeAutoFields) $TBS->LoadTemplate(null,'+'); + } else { + $this->TbsNoField[$idx] = true; + } + } + } + + if ($nbr>0) { + $this->TbsSwitchMode(false); + $this->TbsStorePark(); // save the current file in the store + $this->TbsStoreLoad($SaveIdx, $SaveName); // restore the sub-file as before the QuickLoad + } + + return $nbr; + + } + + function TbsGetFileName($idx) { + if (isset($this->CdFileLst[$idx])) { + return $this->CdFileLst[$idx]['v_name']; + } else { + return '(id='.$idx.')'; + } + } + + /** + * Tells if optimisation marker is prensent in the current source, eventually add it if it is not. + * The optimization marker is a simple space (' ') before the closing chars of the "" element. + * @param string $Txt The text source to check + * @param boolean $mark Set to true to mark the source as done if it is not the case. + * @return boolean True if the current source has just been marked done. Null if it is not possible to telle if it is done or note. Fasle if is is done before. + */ + function TbsApplyOptim(&$Txt, $mark) { + if (substr($Txt, 0, 2) === ''); + if (substr($Txt, $p-1, 1) === ' ') { + return false; + } else { + if ($mark) { + $Txt = substr_replace($Txt, ' ', $p, 0); + } + return true; + } + } else { + return null; + } + } + + /** + * Display the header of the debug mode (only once) + */ + function TbsDebug_Init(&$nl, &$sep, &$bull, $type) { + + static $DebugInit = false; + + if ($DebugInit) return; + $DebugInit = true; + + $nl = "\n"; + $sep = str_repeat('-',30); + $bull = $nl.' - '; + + + if (!headers_sent()) header('Content-Type: text/plain; charset="UTF-8"'); + + echo "* OPENTBS DEBUG MODE: if the star, (*) on the left before the word OPENTBS, is not the very first character of this page, then your +merged Document will be corrupted when you use the OPENTBS_DOWNLOAD option. If there is a PHP error message, then you have to fix it. +If they are blank spaces, line beaks, or other unexpected characters, then you have to check your code in order to avoid them."; + echo $nl; + echo $nl.$sep.$nl.'INFORMATION'.$nl.$sep; + echo $nl.'* Debug command: '.$type; + echo $nl.'* OpenTBS version: '.$this->Version; + echo $nl.'* TinyButStrong version: '.$this->TBS->Version; + echo $nl.'* PHP version: '.PHP_VERSION; + echo $nl.'* Zlib enabled: '.($this->Meth8Ok) ? 'YES' : 'NO (it should be enabled)'; + echo $nl.'* Opened document: '.(($this->ArchFile==='') ? '(none)' : $this->ArchFile); + echo $nl.'* Activated features for document type: '.(($this->ExtInfo===false) ? '(none)' : $this->ExtType.'/'.$this->ExtEquiv); + + } + + function TbsDebug_Info($Exit) { + + $this->TbsDebug_Init($nl, $sep, $bull, 'OPENTBS_DEBUG_INFO'); + + if ($this->ExtInfo !== false) { + + switch ($this->ExtEquiv) { + case 'docx': $this->MsWord_DocDebug($nl, $sep, $bull); break; + case 'xlsx': $this->MsExcel_SheetDebug($nl, $sep, $bull); break; + case 'pptx': $this->MsPowerpoint_SlideDebug($nl, $sep, $bull); break; + case 'ods' : $this->OpenDoc_SheetSlides_Debug(true, $nl, $sep, $bull); break; + case 'odp' : $this->OpenDoc_SheetSlides_Debug(false, $nl, $sep, $bull); break; + } + + switch ($this->ExtType) { + case 'openxml': $this->OpenXML_ChartDebug($nl, $sep, $bull); break; + case 'odf': $this->OpenDoc_ChartDebug($nl, $sep, $bull); break; + } + + } + + if ($Exit) exit; + + } + + /** + * echo() info about modified and added files. + * + * @param boolean $XmlFormat format XML contents + * @param boolean $Current true to start the debug with the current subtemplate, false to start when Show is called. + */ + function TbsDebug_Merge($XmlFormat, $Current) { + + $this->TbsDebug_Init($nl, $sep, $bull, ($Current ? 'OPENTBS_DEBUG_XML_CURRENT' :'OPENTBS_DEBUG_XML_SHOW')); + + // scann files for collecting information + $mod_lst = ''; // id of modified files + $del_lst = ''; // id of deleted files + $add_lst = ''; // id of added files + + // files marked as replaced in TbsZip + $idx_lst = array_keys($this->ReplInfo); + foreach ($idx_lst as $idx) { + $name = $this->TbsGetFileName($idx); + if ($this->ReplInfo[$idx]===false) { + $del_lst .= $bull.$name; + } else { + $mod_lst .= $bull.$name; + } + } + + // files marked as modified in the Park + $idx_lst = array_keys($this->TbsStoreLst); + foreach ($idx_lst as $idx) { + if (!isset($this->ReplInfo[$idx])) { + $mod_lst .= $bull.$this->TbsGetFileName($idx); + } + } + + // files marked as added in TbsZip + $idx_lst = array_keys($this->AddInfo); + foreach ($idx_lst as $idx) { + $name = $this->AddInfo[$idx]['name']; + $add_lst .= $bull.$name; + } + + if ($mod_lst==='') $mod_lst = ' none'; + if ($del_lst==='') $del_lst = ' none'; + if ($add_lst==='') $add_lst = ' none'; + + echo $nl.'* Deleted files in the archive:'.$del_lst; + echo $nl.'* Added files in the archive:'.$add_lst; + echo $nl.'* Modified files in the archive:'.$mod_lst; + echo $nl; + + // display contents merged with OpenTBS + foreach ($this->DebugLst as $name=>$src) { + $x = trim($src); + $info = ''; + $xml = ((strlen($x)>0) && $x[0]==='<'); + if ($XmlFormat && $xml) { + $info = ' (XML reformated for debuging only)'; + $src = $this->XmlFormat($src); + } + echo $nl.$sep; + echo $nl.'File merged with OpenTBS'.$info.': '.$name; + echo $nl.$sep; + echo $nl.$src; + } + + } + + function ConvXmlOnly($Txt, $ConvBr) { + // Used by TBS to convert special chars and new lines. + $x = htmlspecialchars($Txt, ENT_COMPAT); // ENT_COMPAT is no more the default value since PHP 8.1 + if ($ConvBr) $this->ConvBr($x); + return $x; + } + + function ConvXmlUtf8($Txt, $ConvBr) { + // Used by TBS to convert special chars and new lines. + $x = htmlspecialchars(iconv('ISO-8859-1', 'UTF-8', $Txt), ENT_COMPAT); // ENT_COMPAT is no more the default value since PHP 8.1 + if ($ConvBr) $this->ConvBr($x); + return $x; + } + + function ConvBr(&$x) { + $z = $this->TBS->OtbsConvBr; + if ($z===false) return; + $x = nl2br($x); // Convert any type of line break + $x = str_replace("\r", '' ,$x); + $x = str_replace("\n", '' ,$x); + $x = str_replace('
',$z ,$x); + + } + + function XmlFormat($Txt) { + // format an XML source the be nicely aligned + + // delete line breaks + $Txt = str_replace("\r",'',$Txt); + $Txt = str_replace("\n",'',$Txt); + + // init values + $p = 0; + $lev = 0; + $Res = ''; + + $to = true; + while ($to!==false) { + $to = strpos($Txt,'<',$p); + if ($to!==false) { + $tc = strpos($Txt,'>',$to); + if ($to===false) { + $to = false; // anomaly + } else { + // get text between the tags + $x = trim(substr($Txt, $p, $to-$p),' '); + if ($x!=='') $Res .= "\n".str_repeat(' ',max($lev,0)).$x; + // get the tag + $x = substr($Txt, $to, $tc-$to+1); + if ($Txt[$to+1]==='/') $lev--; + $Res .= "\n".str_repeat(' ',max($lev,0)).$x; + // change the level + if (($Txt[$to+1]!=='?') && ($Txt[$to+1]!=='/') && ($Txt[$tc-1]!=='/')) $lev++; + // next position + $p = $tc + 1; + } + } + } + + $Res = substr($Res, 1); // delete the first line break + if ($pTBS->NoErr); + if ($exit) $Msg .= ' The process is ending, unless you set NoErr property to true.'; + $this->TBS->meth_Misc_Alert('OpenTBS Plugin', $Msg, $NoErrMsg); + if ($exit) { + if ($this->DebugLst!==false) { + if ($this->TbsCurrIdx!==false) $this->DebugLst[$this->TbsGetFileName($this->TbsCurrIdx)] = $this->TBS->Source; + $this->TbsDebug_Merge(true, false); + } + exit; + } + return false; + } + + /** + * Return the item of an array if exits, or the default value. + */ + function getItem($array, $item, $default) { + if (isset($array[$item])) { + return $array[$item]; + } else { + return $default; + } + } + + /** + * Prepare the TBS field for merging a picture: the TBS field is moved to the target attribute. + * This is done only once when it is a block merging. + * The actual image replacement is done by $this->TbsPicAdd() + * + * @return boolean Return true if the preparation ends correctly or if it as already been ended correctly before. + */ + function TbsPicPrepare(&$Txt, &$Loc, $IsCaching) { + + if (isset($Loc->PrmLst['pic_prepared'])) { + return true; + } + + if (isset($Loc->PrmLst['att'])) { + return $this->RaiseError('Parameter att is used with parameter ope=changepic in the field ['.$Loc->FullName.']. changepic will be ignored'); + } + + // Direction of search + $backward = true; + if (isset($Loc->PrmLst['tagpos'])) { + $s = $Loc->PrmLst['tagpos']; + if ($s=='before') { + $backward = false; + } elseif ($s=='inside') { + if ($this->ExtType=='openxml') $backward = false; + } + } + + // Find the target attribute + $att = false; + if ($this->ExtType==='odf') { + $att = 'draw:image#xlink:href'; + $magnet = 'draw:frame'; + } elseif ($this->ExtType==='openxml') { + $type = $this->OpenXML_FirstPicType($Txt, $Loc->PosBeg, $backward); + if ($type == 'vml') { + // old way + $att = 'v:imagedata#r:id'; + $magnet = 'w:pict'; + } elseif ($type == 'dml') { + $att = 'a:blip#r:embed'; + $magnet = 'w:drawing'; + } else { + return $this->RaiseError('Parameter ope=changepic used in the field ['.$Loc->FullName.'] has failed to found the picture.'); + } + } else { + return $this->RaiseError('Parameter ope=changepic used in the field ['.$Loc->FullName.'] is not supported with the current document type.'); + } + + $LocDef = substr($Txt, $Loc->PosBeg, $Loc->PosEnd - $Loc->PosBeg + 1); + + // Move the field to the target attribute + // This technical works while caching TBS fields because already cached fields are necessarily placed before the current picture. + $prefix = ($backward) ? '' : '+'; + $Loc->PrmLst['att'] = $prefix.$att; + clsTinyButStrong::f_Xml_AttFind($Txt,$Loc,true); + + // Delete parameter att to prevent TBS from another processing + unset($Loc->PrmLst['att']); + + $Loc->PrmLst['magnet'] = $magnet; + + /* + With an OpenXML document, the TBS field defined in the property Description or Title can be silently duplicated to another entity nearby (usually ). + This will make the picture replacement to be processed twice : one fof each TBS field. But this won't make always error because the external file will be inserted onyl once and the two Rid will be the same. + Nevertheless, if the picture use parameter 'adjust' then this will corrupt the XML when several cached TBS fields have to be merged the the same picture element. + This is because the redim process uses relative cached positioning. + In order to evoid this error and to optimize picture replacement, any duplicated TBS field in the picture element will be neutralized. + */ + $PicLoc = false; + if (isset($this->ExtInfo['pic_entity'])) { + $PicLoc = clsTbsXmlLoc::FindElement($Txt, $this->ExtInfo['pic_entity'], $Loc->PosBeg, false); + if ($PicLoc) { + // We neutralized the duplicated definition but we must keep the current locatore positioning because it is quite complicated for now + $PicLoc->switchToRelative(); + $src = $PicLoc->GetSrc(); + $src = str_replace($LocDef, str_repeat(' ', strlen($LocDef)), $src); // important : same length than $Loc because dim positioning must not change + $PicLoc->ReplaceSrc($src); + $PicLoc->switchToNormal(); + } + } + + // Get picture dimension information + if (isset($Loc->PrmLst['adjust'])) { + $FieldLen = 0; + if ($this->ExtType==='odf') { + $Loc->Prop['otbsDim'] = $this->TbsPicGetDim_ODF($Txt, $Loc->PosBeg, false, $Loc->PosBeg, $FieldLen); + } else { + if (strpos($att,'v:imagedata')!==false) { + $Loc->Prop['otbsDim'] = $this->TbsPicGetDim_OpenXML_vml($Txt, $Loc->PosBeg, false, $Loc->PosBeg, $FieldLen); + } else { + $Loc->Prop['otbsDim'] = $this->TbsPicGetDim_OpenXML_dml($Txt, $Loc->PosBeg, false, $Loc->PosBeg, $FieldLen, $PicLoc); + } + } + } + + // Set the original picture to empty + if ( isset($Loc->PrmLst['unique']) && $Loc->PrmLst['unique'] ) { + + // Get the value in the template + $Value = substr($Txt, $Loc->PosBeg, $Loc->PosEnd - $Loc->PosBeg +1); + + if ($this->ExtType==='odf') { + $InternalPicPath = $Value; + } elseif ($this->ExtType==='openxml') { + $InternalPicPath = $this->OpenXML_GetInternalPicPath($Value); + if ($InternalPicPath === false) { + $this->RaiseError('The picture to merge with field ['.$Loc->FullName.'] cannot be found (Rid = ' . $Value . ').'); + } + } + + // Set the picture file to empty + $this->FileReplace($InternalPicPath, '', TBSZIP_STRING, false); + + } + + $Loc->PrmLst['pic_prepared'] = true; + return true; + + } + + function TbsPicGetDim_ODF($Txt, $Pos, $Forward, $FieldPos, $FieldLen) { + // Found the attributes for the image dimensions, in an ODF file + // unit (can be: mm, cm, in, pi, pt) + $EntityOffset = 0; + $dim = $this->TbsPicGetDim_Any($Txt, $Pos, $Forward, $FieldPos, $FieldLen, $EntityOffset, 'draw:frame', 'svg:width="', 'svg:height="', 3, false, false); + return array($dim); + } + + function TbsPicGetDim_OpenXML_vml($Txt, $Pos, $Forward, $FieldPos, $FieldLen) { + $EntityOffset = 0; + $dim = $this->TbsPicGetDim_Any($Txt, $Pos, $Forward, $FieldPos, $FieldLen, $EntityOffset, 'v:shape', 'width:', 'height:', 2, false, false); + return array($dim); + } + + function TbsPicGetDim_OpenXML_dml($Txt, $Pos, $Forward, $FieldPos, $FieldLen, $PicLoc) { + + $EntityOffset = 0; + + // Loc of the picture entity + if ($PicLoc !== false) { + // The seach is done relatively to the picture entity + $PicLoc->FindEndTag(); + $Txt = $PicLoc->GetSrc(); + $Pos = 0; + $Forward = true; + $EntityOffset = $PicLoc->PosBeg; + } + + $dim_shape = $this->TbsPicGetDim_Any($Txt, $Pos, $Forward, $FieldPos, $FieldLen, $EntityOffset, 'wp:extent', 'cx="', 'cy="', 0, 12700, false); + $dim_inner = $this->TbsPicGetDim_Any($Txt, $Pos, $Forward, $FieldPos, $FieldLen, $EntityOffset, 'a:ext' , 'cx="', 'cy="', 0, 12700, 'uri="'); + $dim_drawing = $this->TbsPicGetDim_Drawings($Txt, $Pos, $FieldPos, $FieldLen, $EntityOffset, $dim_inner); // check for XLSX + + // dims must be sorted in reverse order of location + $result = array(); + if ($dim_shape!==false) $result[$dim_shape['wb']] = $dim_shape; + if ($dim_inner!==false) $result[$dim_inner['wb']] = $dim_inner; + if ($dim_drawing!==false) $result[$dim_drawing['wb']] = $dim_drawing; + krsort($result); + + return $result; + + } + + // Found the attributes for the image dimensions, in any type of file + function TbsPicGetDim_Any($Txt, $Pos, $Forward, $FieldPos, $FieldLen, $EntityOffset, $Element, $AttW, $AttH, $AllowedDec, $CoefToPt, $IgnoreIfAtt) { + + while (true) { + + $p = clsTinyButStrong::f_Xml_FindTagStart($Txt, $Element, true, $Pos, $Forward, true); + if ($p===false) return false; + + $pe = strpos($Txt, '>', $p); + if ($pe===false) return false; + + $src = substr($Txt, $p, $pe -$p); + + if ( ($IgnoreIfAtt===false) || (strpos($src, $IgnoreIfAtt)===false) ) { + + $att_lst = array('w' => $AttW, 'h' => $AttH); + $res_lst = array(); + //$res_lst['debug_src'] = $src; + //$res_lst['debug_entity_offset'] = $EntityOffset; + + foreach ($att_lst as $dim => $att) { + $l = strlen($att); + $b = strpos($src, $att); + if ($b===false) return false; + $b = $b + $l; + $e = strpos($src, '"', $b); + $e2 = strpos($src, ';', $b); // in case of VML format, width and height are styles separted by ; + if ($e2!==false) $e = min($e, $e2); + if ($e===false) return false; + $lt = $e - $b; + $t = substr($src, $b, $lt); + $pu = $lt; // unit first char + while ( ($pu>1) && (!is_numeric($t[$pu-1])) ) $pu--; + $u = ($pu>=$lt) ? '' : substr($t, $pu); + $v = floatval(substr($t, 0, $pu)); + $beg = $EntityOffset + $p + $b; + if ($beg>$FieldPos) $beg = $beg - $FieldLen; + $res_lst[$dim.'b'] = $beg; // start position in the main string + $res_lst[$dim.'l'] = $lt; // length of the text + $res_lst[$dim.'u'] = $u; // unit + $res_lst[$dim.'v'] = $v; // value + $res_lst[$dim.'t'] = $t; // text + $res_lst[$dim.'o'] = 0; // offset + //$res_lst[$dim.'_debug_val'] = substr($Txt, $p+$b, $lt); + //$res_lst[$dim.'_debug_att'] = $att; + } + + $res_lst['r'] = ($res_lst['hv']==0) ? 0.0 : $res_lst['wv']/$res_lst['hv']; // ratio W/H + $res_lst['dec'] = $AllowedDec; // save the allowed decimal for this attribute + $res_lst['cpt'] = $CoefToPt; + return $res_lst; + + } else { + + // Next try + $Pos = $p + (($Forward) ? +1 : -1); + + } + + } + + } + + // Get Dim in an OpenXML Drawing (pictures in an XLSX) + function TbsPicGetDim_Drawings($Txt, $Pos, $FieldPos, $FieldLen, $EntityOffset, $dim_inner) { + + // The coordinates must have been found previously. + if ($dim_inner===false) return false; + // The current file must be an XLSX drawing sub-file. + if (strpos($this->TBS->OtbsCurrFile, 'xl/drawings/')!==0) return false; + + if ($Pos==0) { + // The parent element has already been found + $PosEl = 0; + } else { + // Found parent element + $loc = clsTbsXmlLoc::FindStartTag($Txt, 'xdr:twoCellAnchor', $Pos, false); + if ($loc===false) return false; + $PosEl = $loc->PosBeg; + } + + $loc = clsTbsXmlLoc::FindStartTag($Txt, 'xdr:to', $PosEl, true); + if ($loc===false) return false; + $p = $loc->PosBeg; + + $res = array(); + + $el_lst = array('w'=>'xdr:colOff', 'h'=>'xdr:rowOff'); + foreach ($el_lst as $i=>$el) { + $loc = clsTbsXmlLoc::FindElement($Txt, $el, $p, true); + if ($loc===false) return false; + $beg = $EntityOffset + $loc->GetInnerStart(); + if ($beg>$FieldPos) $beg = $beg - $FieldLen; + $val = $dim_inner[$i.'v']; + $tval = $loc->GetInnerSrc(); + $res[$i.'b'] = $beg; + $res[$i.'l'] = $loc->GetInnerLen(); + $res[$i.'u'] = ''; + $res[$i.'v'] = $val; + $res[$i.'t'] = $tval; + $res[$i.'o'] = intval($tval) - $val; + } + + $res['r'] = ($res['hv']==0) ? 0.0 : $res['wv']/$res['hv']; // ratio W/H; + $res['dec'] = 0; + $res['cpt'] = 12700; + + return $res; + + } + + /** + * Return the path of the image on the server corresponding the current field being merged. + */ + function TbsPicExternalPath(&$Value, &$PrmLst, $Loc) { + + $TBS = &$this->TBS; + + // set the path where files should be taken + if (isset($PrmLst['from'])) { + if (!isset($PrmLst['pic_prepared'])) $TBS->meth_Merge_AutoVar($PrmLst['from'],true); // merge automatic TBS fields in the path + $FullPath = str_replace($TBS->_ChrVal,$Value,$PrmLst['from']); // merge [val] fields in the path + } else { + $FullPath = $Value; + } + if ( (!isset($PrmLst['pic_prepared'])) && isset($PrmLst['default']) ) $TBS->meth_Merge_AutoVar($PrmLst['default'],true); // merge automatic TBS fields in the path + + // check if the picture exists, and eventually use the default picture + if (!file_exists($FullPath)) { + if (isset($PrmLst['default'])) { + $x = $PrmLst['default']; + if ($x==='current') { + return false; + } elseif (file_exists($x)) { + $FullPath = $x; + } else { + return $this->RaiseError('The default picture "'.$x.'" defined by parameter "default" of the field ['.$Loc->FullName.'] is not found.'); + } + } else { + return false; + } + } + + return $FullPath; + + } + + /** + * Add a picture inside the archive, use parameters 'from' and 'as'. + * + * @param string $Value + * @param array $PrmLst + * @param string $Txt + * @param object $Loc + * @param array $Prm Caller parameter. Only used for error messages. + * + * @return boolean Returne true if the picture is correcly replaced or deleted. + */ + function TbsPicAdd(&$Value, &$PrmLst, &$Txt, &$Loc, $Prm) { + + if (isset($PrmLst['pic_canceled'])) { + //$Value = ''; + return false; + } + + if ($Value == '') { + // The magnet parameter will delete the picture container + return true; + } + + $TBS = &$this->TBS; + + $PrmLst['pic_prepared'] = true; // mark the locator as Picture prepared + + // Path of the external file to copy inside the current document. + $ExternalPath = $this->TbsPicExternalPath($Value, $PrmLst, $Loc); + + if ($ExternalPath === false) { + if (isset($PrmLst['att'])) { + // can happen when using MergeField() + unset($PrmLst['att']); + $Value = ''; + } else { + // parameter att already applied during Field caching + $Value = substr($Txt, $Loc->PosBeg, $Loc->PosEnd - $Loc->PosBeg + 1); + } + return false; + } + + // Path to the target file to add into the current document. + if (isset($PrmLst['as'])) { + if (!isset($PrmLst['pic_prepared'])) $TBS->meth_Merge_AutoVar($PrmLst['as'],true); // merge automatic TBS fields in the path + $InternalPath = str_replace($TBS->_ChrVal,$Value,$PrmLst['as']); // merge [val] fields in the path + } else { + // uniqueness by the name of the file, not its full path, this is a weakness + // OpenXML does not support spaces and accents in internal file names. + $x = basename($ExternalPath); + if (!isset($this->ImageInternal[$x])) { + $ext = $this->Misc_FileExt(basename($ExternalPath)); + $this->ImageInternal[$x] = 'opentbs_added_' . $this->ImageIndex . '.' . $ext; + $this->ImageIndex++; + } + $InternalPath = $this->ImageInternal[$x]; + } + + // the value of the current TBS field becomes the full internal path + if (isset($this->ExtInfo['pic_path'])) { + $InternalPath = $this->ExtInfo['pic_path'].$InternalPath; + } + + // actually add the picture inside the archive + if ($this->FileGetIdxAdd($InternalPath)===false) { + $this->FileAdd($InternalPath, $ExternalPath, TBSZIP_FILE, true); + } + + // preparation for others file in the archive + $Rid = false; + if ($this->ExtType==='odf') { + // OpenOffice document + $this->OpenDoc_ManifestChange($InternalPath,''); + } elseif ($this->ExtType==='openxml') { + // Microsoft Office document + $this->OpenXML_CTypesPrepareExt($InternalPath, ''); // add the mime type if missing into the dedicated sub-file + // Add the file into de Relation declaration + $BackNbr = max(substr_count($TBS->OtbsCurrFile, '/') - 1, 0); // docx=>"media/img.png", xlsx & pptx=>"../media/img.png" + $TargetDir = str_repeat('../', $BackNbr).'media/'; + $FileName = basename($InternalPath); + $Rid = $this->OpenXML_Rels_AddNewRid($TBS->OtbsCurrFile, $TargetDir, $FileName); + } + + // change the value of the field for the merging process + if ($Rid===false) { + $Value = $InternalPath; + } else { + $Value = $Rid; // the Rid is used instead of the file name for the merging + } + + // Change the dimensions of the picture + if (isset($Loc->Prop['otbsDim'])) { + $this->TbsPicAdjust($Txt, $Loc, $ExternalPath); + } + + return true; + + } + + function TbsDocFieldPrepare(&$Txt, &$Loc) { + + if ($this->ExtEquiv === 'docx') { + + // Find the first element of the Complexe Field + $loc_beg = clsTbsXmlLoc::FindStartTagHavingAtt($Txt, 'w:fldCharType="begin"', $Loc->PosBeg, false); // find a + if ($loc_beg === false) return; + $loc_beg = clsTbsXmlLoc::FindStartTag($Txt, 'w:r', $loc_beg->PosBeg, false); // the that contains the + + // Find the last element of the Complexe Field + $loc_end = clsTbsXmlLoc::FindStartTagHavingAtt($Txt, 'w:fldCharType="end"', $Loc->PosEnd, true); // find a + if ($loc_end === false) return; + $loc_end = clsTbsXmlLoc::FindElement($Txt, 'w:r', $loc_end->PosBeg, false); // the that contains the + + // The penultimate element is the latest calculated field. We use it for getting the formating text. + $loc_wr = clsTbsXmlLoc::FindElement($Txt, 'w:r', $loc_end->PosBeg - 1, false); + if ($loc_wr === false) { + $x = 'DOCFIELD'; + } else { + // Delete the Complete Field element if any (should not) + $x = $loc_wr->GetSrc(); + $this->XML_DeleteElements($x, array('w:instrText', 'w:fldChar')); + // Replace text + $lz = clsTbsXmlLoc::FindElement($x, 'w:t', 0); + if ($lz === false) { + // Not found => we create a new text before the closing tag + $x = substr_replace($x, 'DOCFIELD', -6, 0); + } else { + $lz->ReplaceInnerSrc('DOCFIELD'); + } + } + + // Replace template + $len = $loc_end->PosEnd - $loc_beg->PosBeg + 1; + $Txt = substr_replace($Txt, $x, $loc_beg->PosBeg, $len); + + // Move the locator + $p = strpos($x, '>DOCFIELD<') + 1; + $Loc->PosBeg = $loc_beg->PosBeg + $p; + $Loc->PosEnd = $Loc->PosBeg + strlen('DOCFIELD') - 1; + + } elseif ($this->ExtEquiv === 'odt') { + + $loc_el = clsTbsXmlLoc::FindStartTagByPrefix($Txt, '', $Loc->PosBeg, false); + if ( ($loc_el !== false) && ($loc_el->Name === 'text:conditional-text') ) { + $loc_el->FindEndTag(); + $Loc->PosBeg = $loc_el->PosBeg; + $Loc->PosEnd = $loc_el->PosEnd; + } + + } + + } + + // Adjust the dimensions if the picture + function TbsPicAdjust(&$Txt, &$Loc, &$File) { + + $fDim = @getimagesize($File); // file dimensions + if (!is_array($fDim)) return; + $w = (float) $fDim[0]; + $h = (float) $fDim[1]; + $r = ($w/$h); + $delta = 0; + $adjust = $Loc->PrmLst['adjust']; + if ( (!is_string($adjust)) || ($adjust=='') ) $adjust = 'inside'; + if (strpos($adjust, '%')!==false) { + $adjust_coef = floatval(str_replace('%','',$adjust))/100.0; + $adjust = '%'; + } + + // Save position of the locator before dims are modified + if (!isset($Loc->Prop['svPosBeg'])) { + $Loc->Prop['svPosBeg'] = $Loc->PosBeg; + $Loc->Prop['svPosEnd'] = $Loc->PosEnd; + } + + foreach ($Loc->Prop['otbsDim'] as $tDim) { // template dimensions. They must be sorted in reverse order of location + if ($tDim!==false) { + // find what dimensions should be edited + if ($adjust=='%') { + if ($tDim['wb']>$tDim['hb']) { // the last attribute must be processed first + $edit_lst = array('w' => $adjust_coef * $w, 'h' => $adjust_coef * $h ); + } else { + $edit_lst = array('h' => $adjust_coef * $h, 'w' => $adjust_coef * $w ); + } + } elseif ($adjust=='samewidth') { + $edit_lst = array('h' => $tDim['wv'] * $h / $w ); + } elseif ($adjust=='sameheight') { + $edit_lst = array('w' => $r * $tDim['hv'] ); + } else { // default value + if ($tDim['r']>=$r) { + $edit_lst = array('w' => $r * $tDim['hv'] ); // adjust width + } else { + $edit_lst = array('h' => $tDim['wv'] * $h / $w ); // adjust height + } + } + // edit dimensions + foreach ($edit_lst as $what =>$new ) { + $beg = $tDim[$what.'b']; + $len = $tDim[$what.'l']; + $unit = $tDim[$what.'u']; + if ($adjust=='%') { + if ($tDim['cpt']!==false) $new = $new * $tDim['cpt']; // apply the coef to Point conversion if any + if ($unit!=='') { // force unit to pt, if units are allowed + $unit = 'pt'; + } + } + $new = $new + $tDim[$what.'o']; // add the offset (xlsx only) + $new = number_format($new, $tDim['dec'], '.', '').$unit; + $Txt = substr_replace($Txt, $new, $beg, $len); + if ($Loc->PosBeg>$beg) $delta = $delta + strlen($new) - $len; + } + } + } + + // Update the position + $Loc->PosBeg = $Loc->Prop['svPosBeg'] + $delta; + $Loc->PosEnd = $Loc->Prop['svPosEnd'] + $delta; + + } + + /** + * Search 1 or 2 strings in a list if several sub-file in the archive. + * + * @param string|array $files An associated array of sub-files to scann or a pattern using 1 wildcard '*'. + * @param string|array $strLst The strings that must be all present in the content of the file. + * It can be a array of strings, or a single string. + * @param boolean $returnFirstFind true to return only the first record found. + * @param boolean $any true to search for any of the string, false to search for all of the strings. + * + * @return array Return a single record or a recordset structured like: array('key'=>, 'idx'=>, 'src'=>, 'pos'=>, 'curr'=>) + */ + function TbsSearchInFiles($files, $strLst, $any, $returnFirstFound) { + + // Prepare variables + + if (is_string($strLst)) { + $strLst = array($strLst); + } + + $keys_todo = array(); // list of keys that remains to be done + $idx_keys = array(); // trancoding idx to key + if (is_array($files)) { + // A list of files given => transform the list of files into a list of available idx + foreach($files as $k => $f) { + $idx = $this->FileGetIdx($f); + if ($idx!==false) { + $keys_todo[$k] = $idx; + $idx_keys[$idx] = $k; + } + } + } else { + // A string is given => + $parts = explode('*', $files); + $last = count($parts) - 1; + if ($last == 0) { + $parts[1] = ''; + $last = 1; + } + $len_0 = strlen($parts[0]); + $len_1 = strlen($parts[1]); + foreach ($this->CdFileByName as $f => $idx) { + if ( ($len_0 == 0) || (substr($f, 0, $len_0) == $parts[0]) ) { + if ( ($len_1 == 0) || (substr($f, -$len_1) == $parts[1]) ) { + $keys_todo[$f] = $idx; + $idx_keys[$idx] = $f; + } + } + } + } + + // Start search + + $result = array(); + + // Search in the current sub-file + if ( ($this->TbsCurrIdx!==false) && isset($idx_keys[$this->TbsCurrIdx]) ) { + $key = $idx_keys[$this->TbsCurrIdx]; + $p = $this->TbsSearchInTxt($this->TBS->Source, $strLst, $any); + if ($p !== false) { + $result[] = array('key' => $key, 'idx' => $this->TbsCurrIdx, 'src' => &$this->TBS->Source, 'pos' => $p, 'curr'=>true); + if ($returnFirstFound) return $result[0]; + } + unset($keys_todo[$key]); + } + + // Search in the store + foreach($this->TbsStoreLst as $idx => $info) { + if ( ($idx!==$this->TbsCurrIdx) && isset($idx_keys[$idx]) ) { + $key = $idx_keys[$idx]; + $p = $this->TbsSearchInTxt($info['src'], $strLst, $any); + if ($p !== false) { + $result[] = array('key' => $key, 'idx' => $idx, 'src' => &$info['src'], 'pos' => $p, 'curr'=>false); + if ($returnFirstFound) return $result[0]; + } + unset($keys_todo[$key]); + } + } + + // Search in other sub-files (never opened) + foreach ($keys_todo as $key => $idx) { + $txt = $this->FileRead($idx); + $p = $this->TbsSearchInTxt($txt, $strLst, $any); + if ($p !== false) { + $result[] = array('key' => $key, 'idx' => $idx, 'src' => $txt, 'pos' => $p, 'curr'=>false); + if ($returnFirstFound) return $result[0]; + } + } + + if ($returnFirstFound) { + return array('key'=>false, 'idx'=>false, 'src'=>false, 'pos'=>false, 'curr'=>false); + } else { + return $result; + } + + } + + /** + * Search for a liste of strings in a main string. + * + * @param string $txt The string to search into. + * @param array $strLst An array of strings to search in $txt. + * @param boolean $any True to search for any of the string, false to search for all of the strings. + * + * @return integer|false The position of the first item found, or false if none. + */ + function TbsSearchInTxt($txt, $strLst, $any) { + + if ($any) { + // Any of the strings + foreach ($strLst as $s) { + $p = strpos($txt, $s); + if ($p !== false) { + return $p; + } + } + return false; + } else { + // All of the strings + $p = true; + foreach ($strLst as $s) { + if ($p !== false) { + $p = strpos($txt, $s); + } + } + return $p; + } + + } + + // Check after the sheet process + function TbsSheetCheck() { + if (count($this->OtbsSheetSlidesDelete)>0) $this->RaiseError("Unable to delete the following sheets because they are not found in the workbook: ".(str_replace(array('i:','n:'),'',implode(', ',$this->OtbsSheetSlidesDelete))).'.'); + if (count($this->OtbsSheetSlidesVisible)>0) $this->RaiseError("Unable to change visibility of the following sheets because they are not found in the workbook: ".(str_replace(array('i:','n:'),'',implode(', ',array_keys($this->OtbsSheetSlidesVisible)))).'.'); + } + + function TbsDeleteComments($MainTags, $CommFiles, $CommTags, $Inner) { + + $nbr = 0; + + // Retrieve the Comment sub-file (OpenXML only) + if ($CommFiles!==false) { + $Files = $this->OpenXML_MapGetFiles($CommFiles); + foreach ($Files as $file) { + $idx = $this->FileGetIdx($file); + if ($idx!==false) { + // Delete inner text of the comments to be sure that contents is deleted + // we only empty the comment elements in case some comments are referenced in other special part of the document + $Txt = $this->TbsStoreGet($idx, "Delete Comments"); + $nbr = $nbr + $this->XML_DeleteElements($Txt, $CommTags, $Inner); + $this->TbsStorePut($idx, $Txt); + } + } + } + + // Retrieve the Main sub-file + if ($MainTags!==false) { + $idx = $this->Ext_GetMainIdx(); + if ($idx===false) return false; + // Delete Comment locators + $Txt = $this->TbsStoreGet($idx, "Delete Comments"); + $nbr2 = $this->XML_DeleteElements($Txt, $MainTags); + $this->TbsStorePut($idx, $Txt); + if ($CommFiles===false) $nbr = $nbr2; + } + + return $nbr; + + } + + /** + * Replace var fields in a Parameter of a block. + * @param string $PrmVal The parameter value. + * @param string $FldVal The value of the field that holds the value. + * @return string The merged value of the parameter. + */ + function TbsMergeVarFields($PrmVal, $FldVal) { + if ($PrmVal === true) $PrmVal = ''; // TBS set the value to true if no value set, but it is converted into '1'. + $this->TBS->meth_Merge_AutoVar($PrmVal, true); + $PrmVal = str_replace($this->TBS->_ChrVal, $FldVal, $PrmVal); + return $PrmVal; + } + + function TbsDeleteColumns(&$Txt, $Value, $PrmLst, $PosBeg) { + + $ext = $this->ExtEquiv; + if ($ext==='docx') { + $el_table = 'w:tbl'; + $el_delete = array( + array('p'=>'w:tblGrid', 'c'=>'w:gridCol', 's'=>false), + array('p'=>'w:tr', 'c'=>'w:tc', 's'=>false), + ); + } elseif ($ext==='odt') { + $el_table = 'table:table'; + $el_delete = array( + array('p'=>false, 'c'=>'table:table-column', 's'=>'table:number-columns-repeated'), + array('p'=>'table:table-row', 'c'=>'table:table-cell', 's'=>false), + ); + } else { + return false; + } + + if (is_array($Value)) $Value = implode(',', $Value); + + // Column set + $shift_ok = false; + if (isset($PrmLst['colset'])) { + $col_set = str_replace(' ', '', $PrmLst['colset']); + $col_set = explode('|', $col_set); + $col_lst = array(); + $val_lst = explode(',', $Value); + foreach ($val_lst as $s) { + $idx = intval($s) - 1; + if (isset($col_set[$idx])) { + $col_lst[] = $col_set[$idx]; + } + } + $col_lst = implode(',', $col_lst); + } elseif (isset($PrmLst['colnum'])) { + // Retreive the list of columns id to delete + $col_lst = $this->TbsMergeVarFields($PrmLst['colnum'], $Value); // prm equal to true if value is not given + $shift_ok = true; + } else { + $col_lst = $Value; + $shift_ok = true; + } + + // Convert the colmun list into an array + $col_lst = str_replace(' ', '', $col_lst); + if ( ($col_lst=='') || ($col_lst=='0') ) return false; // there is nothing to do + $col_lst = explode(',', $col_lst); + $col_nbr = count($col_lst); + for ($c = 0; $c < $col_nbr; $c++) { + // In cas of range separator + $x = explode('-', $col_lst[$c]); + $x0 = intval($x[0]); + $col_lst[$c] = $x0; + // Range separator + if (isset($x[1])) { + $x1 = intval($x[1]); + for ($i = $x0 + 1 ; $i <= $x1 ; $i++) { + $col_lst[] = $i; // added after the existing values + } + } + } + + // Add columns by shifting + if ($shift_ok && isset($PrmLst['colshift'])) { + $col_shift = intval($this->TbsMergeVarFields($PrmLst['colshift'], $Value)); + if ($col_shift<>0) { + $step = ($col_shift>0) ? -1 : +1; + for ($s = $col_shift; $s<>0; $s = $s + $step) { + for ($c=0; $c<$col_nbr; $c++) $col_lst[] = $col_lst[$c] + $s; + } + } + } + + // prepare column info + $col_lst = array_unique($col_lst, SORT_NUMERIC); // Delete duplicated columns + sort($col_lst, SORT_NUMERIC); // Sort colmun id in order + $col_max = $col_lst[(count($col_lst)-1)]; // Last column to delete + + // Delete impossible col num (like zero) + while ( (count($col_lst) > 0) && ($col_lst[0] <= 0) ) { + array_shift($col_lst); + } + if (count($col_lst) == 0) return false; + + // Look for the source of the table + $Loc = clsTbsXmlLoc::FindElement($Txt, $el_table, $PosBeg, false); + if ($Loc===false) return false; + + $Src = $Loc->GetSrc(); + + foreach ($el_delete as $info) { + if ($info['p']===false) { + $this->XML_DeleteColumnElements($Src, $info['c'], $info['s'], $col_lst, $col_max); + } else { + $ParentPos = 0; + while ($ParentLoc = clsTbsXmlLoc::FindElement($Src, $info['p'], $ParentPos, true)) { + $ParentSrc = $ParentLoc->GetSrc(); + $ModifNbr = $this->XML_DeleteColumnElements($ParentSrc, $info['c'], $info['s'], $col_lst, $col_max); + if ($ModifNbr>0) $ParentLoc->ReplaceSrc($ParentSrc); + $ParentPos = $ParentLoc->PosEnd + 1; + } + } + } + + $Loc->ReplaceSrc($Src); + + } + + /** + * Delete or Display a Sheet or a Slide according to its numbre or its name + * @param $id_or_name Id or Name of the Sheet/Slide + * @param $ok true to Keep or Display, false to Delete or Hide + * @param $delete true to Delete/Keep, false to Display/Hide + */ + function TbsSheetSlide_DeleteDisplay($id_or_name, $ok, $delete) { + + if (is_null($ok)) $ok = true; // default value + + + $ext = $this->ExtEquiv; + + $ok = (boolean) $ok; + if (!is_array($id_or_name)) $id_or_name = array($id_or_name); + + foreach ($id_or_name as $item=>$action) { + if (!is_bool($action)) { + $item = $action; + $action = $ok; + } + $item_ref = (is_string($item)) ? 'n:'.htmlspecialchars($item, ENT_COMPAT) : 'i:'.$item; // help to make the difference beetween id and name + if ($delete) { + if ($ok) { + $this->OtbsSheetSlidesDelete[$item_ref] = $item; + } else { + unset($this->OtbsSheetSlidesVisible[$item_ref]); + } + } else { + $this->OtbsSheetSlidesVisible[$item_ref] = $ok; + } + } + + } + + /** + * Prepare the locator for merging cells. + */ + function TbsPrepareMergeCell(&$Txt, &$Loc) { + if ($this->ExtEquiv=='docx') { + // Move the locator just inside the element. + // See OnOperation() for other process + $xml = clsTbsXmlLoc::FindStartTag($Txt, 'w:tcPr', $Loc->PosBeg, false); + if ($xml) { + $Txt = substr_replace($Txt, '', $Loc->PosBeg, $Loc->PosEnd - $Loc->PosBeg + 1); + $Loc->PosBeg = $xml->PosEnd+1; + $Loc->PosEnd = $xml->PosEnd; + $this->PrevVals[$Loc->FullName] = ''; // the previous value is saved in property because they can be several sections, and thus several Loc for the same column. + //$Loc->Prms['strconv']='no'; // should work + $Loc->ConvStr=false; + } + } + } + + + + /** + * Actualize property ExtInfo (Extension Info). + * ExtInfo will be an array with keys 'load', 'br', 'ctype' and 'pic_path'. Keys 'rpl_what' and 'rpl_with' are optional. + * load: files in the archive to be automatically loaded by OpenTBS when the archive is loaded. Separate files with comma ';'. + * br: string that replace break-lines in the values merged by TBS, set to false if no conversion. + * frm: format of the file ('odf' or 'openxml'), for now it is used only to activate a special feature for openxml files + * ctype: (optional) the Content-Type header name that should be use for HTTP download. Omit or set to '' if not specified. + * pic_path: (optional) the folder nale in the archive where to place pictures + * rpl_what: (optional) string to replace automatically in the files when they are loaded. Can be a string or an array. + * rpl_with: (optional) to be used with 'rpl_what', Can be a string or an array. + * User can define his own Extension Information, they are taken in acount if saved int the global variable $_OPENTBS_AutoExt. + */ + function Ext_PrepareInfo($Ext=false) { + + $this->ExtEquiv = false; + $this->ExtType = false; + + // Find the extension + if ($Ext===false) { + $Ext = basename($this->ArchFile); + $p = strrpos($Ext, '.'); + $Ext = ($p===false) ? '' : strtolower(substr($Ext, $p + 1)); + // At this point, $Ext may have special value '' or 'zip' (no extension in the the template file from a local file or stream file). + } + + $Frm = $this->Ext_DeductFormatFromExt($Ext); + if ($Frm===false) { + $Frm = $this->Ext_DeductFormatFromContents($Ext); // may force $Ext to a valid extension + } + + $TBS = &$this->TBS; + $set_option = method_exists($TBS, 'SetOption'); + + $i = false; + $block_alias = false; + + + if (isset($GLOBAL['_OPENTBS_AutoExt'][$Ext])) { + // User defined information + $i = $GLOBAL['_OPENTBS_AutoExt'][$Ext]; + if (isset($i['equiv'])) $this->ExtEquiv = $i['equiv']; + if (isset($i['frm'])) $this->ExtType = $i['frm']; + } elseif ($Frm==='odf') { + // OpenOffice & LibreOffice documents + $i = array('main' => 'content.xml', 'br' => '', 'ctype' => 'application/vnd.oasis.opendocument.', 'pic_path' => 'Pictures/', 'rpl_what' => ''', 'rpl_with' => '\''); + if ($this->FileExists('styles.xml')) $i['load'] = array('styles.xml'); // styles.xml may contain header/footer contents + if ($Ext==='odf') $i['br'] = false; + if ($Ext==='odm') $this->ExtEquiv = 'odt'; + if ($Ext==='ots') $this->ExtEquiv = 'ods'; + $this->ExtType = 'odf'; + $ctype = array('t' => 'text', 's' => 'spreadsheet', 'g' => 'graphics', 'f' => 'formula', 'p' => 'presentation', 'm' => 'text-master'); + $z = substr($Ext, 2, 1); + if (isset($ctype[$z])) { + $i['ctype'] .= $ctype[$z]; + } + $i['pic_ext'] = array('png' => 'png', 'bmp' => 'bmp', 'gif' => 'gif', 'jpg' => 'jpeg', 'jpeg' => 'jpeg', 'jpe' => 'jpeg', 'jfif' => 'jpeg', 'tif' => 'tiff', 'tiff' => 'tiff'); + $block_alias = array( + 'tbs:p' => 'text:p', // ODT+ODP + 'tbs:title' => 'text:h', // ODT+ODP + 'tbs:section' => 'text:section', // ODT + 'tbs:table' => 'table:table', // ODT (sheet for ODS) + 'tbs:row' => 'table:table-row', // ODT+ODS + 'tbs:cell' => 'table:table-cell', // ODT+ODS + 'tbs:comment' => 'office:annotation', + 'tbs:page' => array(&$this, 'OpenDoc_GetPage'), // ODT + 'tbs:slide' => 'draw:page', // ODP + 'tbs:sheet' => 'table:table', // ODS (table for ODT) + 'tbs:draw' => array(&$this, 'OpenDoc_GetDraw'), + 'tbs:drawgroup' => 'draw:g', + 'tbs:drawitem' => array(&$this, 'OpenDoc_GetDraw'), + 'tbs:listitem' => 'text:list-item', // ODT+ODP + ); + if ($set_option) { + $TBS->SetOption('parallel_conf', 'tbs:table', + array( + 'parent' => 'table:table', + 'ignore' => array('table:covered-table-cell', 'table:table-header-rows'), + 'cols' => array('table:table-column' => 'table:number-columns-repeated'), + 'rows' => array('table:table-row'), + 'cells' => array('table:table-cell' => 'table:number-columns-spanned'), + ) + ); + } + } elseif ($Frm==='openxml') { + // Microsoft Office documents + $this->OpenXML_MapInit(); + if ($TBS->OtbsConvertApostrophes) { + $x = array(chr(226) . chr(128) . chr(152), chr(226) . chr(128) . chr(153)); + } else { + $x = null; + } + $ctype = 'application/vnd.openxmlformats-officedocument.'; + if ( ($Ext==='docx') || ($Ext==='docm') ) { + // Notes: (1) '' works but '' enforce compatibility with Libre Office. (2) Line-breaks merged in attributes will corrupt the DOCX anyway. + $i = array('br' => '', 'ctype' => $ctype . 'wordprocessingml.document', 'pic_path' => 'word/media/', 'rpl_what' => $x, 'rpl_with' => '\'', 'pic_entity'=>'w:drawing'); + if ($Ext==='docm') $i['ctype'] = 'application/vnd.ms-word.document.macroEnabled.12'; + $i['main'] = $this->OpenXML_MapGetMain('wordprocessingml.document.main+xml', 'word/document.xml'); + $i['load'] = $this->OpenXML_MapGetFiles(array('wordprocessingml.header+xml', 'wordprocessingml.footer+xml')); + $this->ExtEquiv = 'docx'; + $block_alias = array( + 'tbs:p' => 'w:p', + 'tbs:title' => 'w:p', + 'tbs:section' => array(&$this, 'MsWord_GetSection'), + 'tbs:table' => 'w:tbl', + 'tbs:row' => 'w:tr', + 'tbs:cell' => 'w:tc', + 'tbs:page' => array(&$this, 'MsWord_GetPage'), + 'tbs:draw' => array(&$this, 'MsWord_GetDraw'), + 'tbs:drawgroup' => array(&$this, 'MsWord_GetDraw'), + 'tbs:drawitem' => 'wps:wsp', + 'tbs:listitem' => 'w:p', + ); + if ($set_option) { + $TBS->SetOption('parallel_conf', 'tbs:table', + array( + 'parent' => 'w:tbl', + 'ignore' => array('w:tblPr', 'w:tblGrid'), + 'cols' => array('w:gridCol' => ''), + 'rows' => array('w:tr'), + 'cells' => array('w:tc' => ''), // + ) + ); + } + } elseif ( ($Ext==='xlsx') || ($Ext==='xlsm')) { + $this->MsExcel_DeleteCalcChain(); + $i = array('br' => false, 'ctype' => $ctype . 'spreadsheetml.sheet', 'pic_path' => 'xl/media/', 'pic_entity'=>'xdr:twoCellAnchor'); + if ($Ext==='xlsm') $i['ctype'] = 'application/vnd.ms-excel.sheet.macroEnabled.12'; + $i['main'] = $this->OpenXML_MapGetMain('spreadsheetml.worksheet+xml', 'xl/worksheets/sheet1.xml'); + $this->ExtEquiv = 'xlsx'; + $block_alias = array( + 'tbs:row' => 'row', + 'tbs:cell' => 'c', + 'tbs:draw' => 'xdr:twoCellAnchor', + 'tbs:drawgroup' => 'xdr:twoCellAnchor', + 'tbs:drawitem' => 'xdr:sp', + ); + } elseif ( ($Ext==='pptx') || ($Ext==='pptm') ){ + $i = array('br' => false, 'ctype' => $ctype . 'presentationml.presentation', 'pic_path' => 'ppt/media/', 'rpl_what' => $x, 'rpl_with' => '\'', 'pic_entity'=>'p:pic'); + if ($Ext==='pptm') $i['ctype'] = 'application/vnd.ms-powerpoint.presentation.macroEnabled.12'; + $this->MsPowerpoint_InitSlideLst(); + $i['main'] = (isset($this->OpenXmlSlideLst[0])) ? $this->OpenXmlSlideLst[0]['file'] : 'ppt/slides/slide1.xml'; + $i['load'] = $this->OpenXML_MapGetFiles(array('presentationml.notesSlide+xml')); // auto-load comments + $this->ExtEquiv = 'pptx'; + $block_alias = array( + 'tbs:p' => 'a:p', + 'tbs:title' => 'a:p', + 'tbs:table' => 'a:tbl', + 'tbs:row' => 'a:tr', + 'tbs:cell' => 'a:tc', + 'tbs:draw' => 'p:sp', + 'tbs:drawgroup' => 'p:grpSp', + 'tbs:drawitem' => 'p:sp', + 'tbs:listitem' => 'a:p', + ); + } + $i['pic_ext'] = array('png' => 'png', 'bmp' => 'bmp', 'gif' => 'gif', 'jpg' => 'jpeg', 'jpeg' => 'jpeg', 'jpe' => 'jpeg', 'tif' => 'tiff', 'tiff' => 'tiff', 'ico' => 'x-icon', 'svg' => 'svg+xml'); + } + + if ($i!==false) { + $i['ext'] = $Ext; + if (!isset($i['load'])) $i['load'] = array(); + $i['load'][] = $i['main']; // add to main file at the end of the files to load + } + + if ($set_option && ($block_alias!==false)) $TBS->SetOption('block_alias', $block_alias); + + $this->ExtInfo = $i; + if ($this->ExtEquiv===false) $this->ExtEquiv = $Ext; + if ($this->ExtType===false) $this->ExtType = $Frm; + return (is_array($i)); // return true if the extension is supported + } + + // Return the type of document corresponding to the given extension. + function Ext_DeductFormatFromExt($Ext) { + if (strpos(',odt,ods,odg,odf,odp,odm,ott,ots,otg,otp,', ',' . $Ext . ',') !== false) return 'odf'; + if (strpos(',docx,docm,xlsx,xlsm,pptx,pptm,', ',' . $Ext . ',') !== false) return 'openxml'; + return false; + } + + // Return the type of document corresponding to the inner contents. + function Ext_DeductFormatFromContents(&$Ext) { + if ($this->FileExists('content.xml')) { + // OpenOffice documents + $meta = 'META-INF/manifest.xml'; + if ($this->FileExists($meta)) { + $prefix = 'application/vnd.oasis.opendocument.'; + $txt = $this->FileRead($meta, true); + if (strpos($txt, $prefix.'text') !== false) { + $Ext = 'odt'; + } elseif (strpos($txt, $prefix.'presentation') !== false) { + $Ext = 'odp'; + } elseif (strpos($txt, $prefix.'spreadsheet') !== false) { + $Ext = 'ods'; + } + return 'odf'; + } + } elseif ($this->FileExists('[Content_Types].xml')) { + // Ms Office documents + if ($this->FileExists('word/document.xml')) { + $Ext = 'docx'; + return 'openxml'; + } elseif ($this->FileExists('xl/workbook.xml')) { + $Ext = 'xlsx'; + return 'openxml'; + } elseif ($this->FileExists('ppt/presentation.xml')) { + $Ext = 'pptx'; + return 'openxml'; + } + } + return false; + } + + // Return the idx of the main document, if any. + function Ext_GetMainIdx() { + if ( ($this->ExtInfo!==false) && isset($this->ExtInfo['main']) ) { + return $this->FileGetIdx($this->ExtInfo['main']); + } else { + return false; + } + } + + /** + * Search the next tag of the asked type searching forward. (Not specific to MsWord, works for any XML) + * @param string $Txt + * @param string $Tag must be prefixed with '<' or '') ) { + return $p; + } else { + $p = $p+$len; + } + } + return false; + } + + /** + * Delete all tags of the types given in the list. + * @param {string} $Txt The text content to search into. + * @param {array} $TagLst List of tag names to delete. + * @param {boolean} $OnlyInner Set to true to keep the content inside the element. Set to false to delete the entire element. Default is false. + */ + function XML_DeleteElements(&$Txt, $TagLst, $OnlyInner=false) { + $nb = 0; + $Content = !$OnlyInner; + foreach ($TagLst as $tag) { + $p = 0; + while ($x = clsTbsXmlLoc::FindElement($Txt, $tag, $p)) { + $x->Delete($Content); + $p = $x->PosBeg; + $nb++; + } + } + return $nb; + } + + /** + * Delete all column elements according to their position. + * Return the number of deleted elements. + */ + function XML_DeleteColumnElements(&$Txt, $Tag, $SpanAtt, $ColLst, $ColMax) { + + $ColNum = 0; + $ColPos = 0; + $ColQty = 1; + $Continue = true; + $ModifNbr = 0; + + while ($Continue && ($Loc = clsTbsXmlLoc::FindElement($Txt, $Tag, $ColPos, true)) ) { + + // get colmun quantity covered by the element (1 by default) + if ($SpanAtt!==false) { + $ColQty = $Loc->GetAttLazy($SpanAtt); + $ColQty = ($ColQty===false) ? 1 : intval($ColQty); + } + // count column to keep + $KeepQty = 0; + for ($i=1; $i<=$ColQty ;$i++) { + if (array_search($ColNum+$i, $ColLst)===false) $KeepQty++; + } + if ($KeepQty==0) { + // delete the tag + $Loc->ReplaceSrc(''); + $ModifNbr++; + } else { + if ($KeepQty!=$ColQty) { + // edit the attribute + $Loc->ReplaceAtt($SpanAtt, $KeepQty); + $ModifNbr++; + } + $ColPos = $Loc->PosEnd + 1; + } + + $ColNum += $ColQty; + if ($ColNum>$ColMax) $Continue = false; + } + + return $ModifNbr; + + } + + /** + * Read or write an attribute's value or an entity's value in the first element in a given sub-file. + * + * @param {mixed} $SubFile : the name or the index of the sub-file. Use value false to get the current sub-file. + * @param {string} $ElPath : path of the element. For example : 'w:document/w:body/w:p'. + * @param {string|boolean} $Att : the attribute, or false to replace the entity's value. + * @param {string|boolean} $NewVal : the new value, or false to delete the attribute, or null to return the attribute’s value without writing. + * + * @return {string|boolean} Reading : return true if the attribute is found and processed. False otherwise. + * Writing : return the value as a string, of false if the attribute or the entity is not found. + * return false if $Att = false and the entity is a self-closing tag. + */ + function XML_ReadWriteAtt($SubFile, $ElPath, $Att, $NewVal, $AddElIfMissing = false) { + + // Find the file + if ($SubFile === false) { + $idx = $this->TbsCurrIdx; + } else { + $idx = $this->FileGetIdx($SubFile); + } + if ($idx === false) return false; + $Txt = $this->TbsStoreGet($idx, 'XML_ReadWriteAtt'); + + // Find the element + $el_lst = explode('/', $ElPath); + $p = 0; + $el_idx = 0; + $el_nb = count($el_lst); + $end = $el_nb; + $loc = false; + $loc_prev = false; + while ($el_idx < $end) { + $loc_prev = $loc; + $loc = clsTbsXmlLoc::FindStartTag($Txt, $el_lst[$el_idx], $p); + if ($loc === false) { + if ($AddElIfMissing) { + // stop the loop + $end = $el_idx; + } else { + return false; + } + } else { + $p = $loc->PosEnd; + $el_idx++; + } + } + + if (($loc === false) && ($loc_prev === false)) return false; + + $save = true; + if ($el_idx < $el_nb) { + // One of the entities is not found => create entities + if (is_null($NewVal)) { + return false; + } elseif ($NewVal === false) { + // Nothing to do + $save = false; + } else { + $before = ''; + $after = ''; + $i_end = ($end - 1); + for ($i = $el_idx ; $i < $i_end ; $i++) { + $before .= '<' . $el_lst[$i] . '>'; + $after = '' . $after; + } + if ($Att === false) { + $x = $before . '<' . $el_lst[$i] . '>' . $NewVal . '' . $after; + } else { + $x = $before . '<' . $el_lst[$i] . ' ' . $Att . '="' . $NewVal . '" />' . $after; + } + $loc_prev->FindEndTag(); + if ($loc_prev->pET_PosBeg === false) { + return $this->RaiseError("Cannot apply attribute because entity '" . $loc_prev->FindName() . "' has no ending tag in file [$SubFile]."); + } + $Txt = substr_replace($Txt, $x, $loc_prev->pET_PosBeg, 0); + } + } else { + // The last entity is found + if (is_null($NewVal)) { + // Read + if ($Att === false) { + // read the entity + $loc->FindEndTag(); + return $loc->GetInnerSrc(); + } else { + // read the attribute + return $loc->GetAttLazy($Att); + } + } elseif ($NewVal === false) { + // Delete + if ($Att === false) { + // delete the entity + $loc->Delete(); + } else { + // delete the attribute + $loc->DeleteAtt($Att); + } + } else { + // Modifiy + if ($Att === false) { + // change the entity's value + $loc->FindEndTag(); + $loc->ReplaceInnerSrc($NewVal); + } else { + // change the attribute's value + $loc->ReplaceAtt($Att, $NewVal, true); + } + } + } + + // Save the file + if ($save) { + $this->TbsStorePut($idx, $Txt); + } + + return true; + + } + + /** + * Function used by Block Alias + * The first start tag on the left is supposed to be the good one. + * Note: encapsulation is not yet supported in this version. + */ + function XML_BlockAlias_Prefix($TagPrefix, $Txt, $PosBeg, $Forward, $LevelStop) { + + $loc = clsTbsXmlLoc::FindStartTagByPrefix($Txt, $TagPrefix, $PosBeg, false); + + if ($Forward) { + $loc->FindEndTag(); + return $loc->PosEnd; + } else { + return $loc->PosBeg; + } + + } + + /** + * Return the next cell of the range (actual or virtual). Return false if there is no more cells for this range in the sheet. + * The process assumes that : + * - trailing rows of the range can be misssing in the sheet but there is no missing row between rows. + * - trailing cols of the range can be misssing in a row but there is no missing cell between cells. + * If "$AddMissing = false" : + * Cells of missing columns are return by the function as a valid object with the property "Exists = false". + * Cells of missing rows are return by the function as false. + * But missing cells are skiped in case of a range with with full columns. + * + * @param string|object $SheetLoc The locator of the sheet entity that directly contains row. + * @param array $Range A range info formated as array('cs'=>...,'rs'=>...,'ce'=>...,'re'=>...) + * @param object $PrevLoc The previous locator returned by the function. + * @param string $RowEl Name of the XML entity for rows. + * @param string $CellEl Name of the XML entity for cells. + * @param boolean $AddMissRow True means that an empty row in inserted in order to finish the range visit. + * + * @return object The clsTbsXmlCellReader object of the cell element, with extra properties info : cellCol, cellRow + * Note that is can be a not existing item if the asked range goes out of the sheet. + */ + function XML_GetNextCellLoc(&$SheetLoc, $Range, $PrevLoc, $RowEl, $CellEl, $AttRowR, $AttCellR, $AddMissing) { + + $debug = false; + // Retreive previous and current cell coordinates + if ( $PrevLoc === false ) { + $rowLoc = false; + $currRow = 0; + $currCol = 0; + $targetRow = $Range['rs']; + $targetCol = $Range['cs']; + $currRowOk = true; + $currColOk = true; + $r_pos = 0; + $c_pos = 0; + } else { + $repeat = false; + $rowLoc = $PrevLoc->Parent; + $currRow = $PrevLoc->cellRow + ($rowLoc->RepeatMax - $rowLoc->RepeatIdx); + $currCol = $PrevLoc->cellCol; + $targetCol = $currCol + 1; + $same_row = ($targetCol <= $Range['ce']); + if ($Range['cfull'] && (!$PrevLoc->Exists)) { + $same_row = false; + } + if ($same_row) { + // we will search next cell in the same row + $targetRow = $currRow; + if ($PrevLoc->RepeatIdx < $PrevLoc->RepeatMax) { + // the cell is repeated + $PrevLoc->RepeatIdx++; + $repeat = $PrevLoc; + } elseif (isset($rowLoc->CellLst[$targetCol])) { + // the row is repeated + $repeat = $rowLoc->CellLst[$targetCol]; + } else { + // no repeated + $c_pos = $PrevLoc->PosEnd + 1; + $currColOk = $PrevLoc->Exists; + } + } else { + // we will search the first cell of the range in the next row + $currCol = 0; + $targetCol = $Range['cs']; + $targetRow = $currRow + 1; + if ($targetRow > $Range['re']) { + return false; + } + // we look if the row is repeated + if ($rowLoc->RepeatIdx < $rowLoc->RepeatMax) { + $rowLoc->RepeatIdx++; + $repeat = $rowLoc->CellLst[$targetCol]; + } else { + $c_pos = 0; + $currColOk = true; + } + } + // Return the repeated cell if any + if ($repeat !== false) { + if (!isset($targetRow)) exit("\n oups"); + if ($debug) echo "\n* XML_GetNextCellLoc CELLULE REPETEE : targetRow = $targetRow, targetCol = $targetCol"; + $repeat->cellCol = $targetCol; + $repeat->cellRow = $targetRow; + return $repeat; + } + $currRowOk = $PrevLoc->RowOk; + $r_pos = $PrevLoc->Parent->PosEnd + 1; + } + + + // Moves to the next cell + + if ($debug) echo "\n* XML_GetNextCellLoc : currRow = $currRow, currCol = $currCol | targetRow = $targetRow, targetCol = $targetCol | currRowOk = ".var_export($currRowOk,true).", currColOk = ".var_export($currColOk,true); + + // Reach the asked row + if ($debug) echo "\n * Search Row : loop"; + while ( $currRowOk && ($currRow < $targetRow) ) { + if ($debug) echo "\n * Search Row #{$currRow}, r_pos=$r_pos : "; + if ( ($rowLoc !== false) && ($rowLoc->RepeatIdx < $rowLoc->RepeatMax) ) { + if ($debug) echo "ok REPEATED"; + // It is a repated row + $rowLoc->RepeatIdx++; + $currRow++; + } else { + $rowLoc = clsTbsXmlCellReader::FindElement($SheetLoc, $RowEl, $r_pos, true); + if ($rowLoc === false) { + if ($debug) echo "FAIL row not found"; + $currRowOk = false; + } else { + if ($debug) echo "ok found"; + $r_pos = $rowLoc->PosEnd + 1; + $currRow++; + // Repeat info + $rowLoc->RepeatIdx = 1; + $rowLoc->RepeatMax = 1; + $rowLoc->CellLst = array(); + if ($AttRowR) { + $max = $rowLoc->GetAttLazy($AttRowR); + if ($max !== false) { + $rowLoc->RepeatMax = intval($max); + } + } + } + } + } + + // Insert missing rows + if ($debug) echo "\n * Insert Row : check"; + if (!$currRowOk) { + $currColOk = false; + if ($AddMissing) { + // Insert the empty rows + $x = '<' . $RowEl . '>'; + $x_len = strlen($x); + $nb = ($targetRow - $currRow); + $SheetLoc->Txt = substr_replace($SheetLoc->Txt, str_repeat($x, $nb), $r_pos, 0); + // The row locator must be targeted on the last inserted row + $r_pos = $r_pos + ($nb - 1) * $x_len; + $rowLoc = new clsTbsXmlCellReader($SheetLoc->Txt, $RowEl, $r_pos, null, $SheetLoc, false); + $rowLoc->FindEndTag(); + } else { + // No more data + return false; + } + } + + // Reach the asked cell + if ($debug) echo "\n * Search Col : loop : currColOk=" . var_export($currColOk, true) . ", currCol=$currCol, targetCol=$targetCol"; + $cellLoc = false; + while ($currColOk && ($currCol < $targetCol)) { + if ($rowLoc === false) return $this->RaiseError("No parent row."); + if (isset($rowLoc->CellLst[$currCol])) return $this->RaiseError("This repeated row should have been previsouly catched."); + if ($debug) echo "\n * Search Col (c_pos=$c_pos) : "; + if ( ($cellLoc !== false) && ($cellLoc->RepeatIdx < $cellLoc->RepeatMax) ) { + if ($debug) echo "ok REPEATED"; + // It is a repated row + $cellLoc->RepeatIdx++; + $currCol++; + } else { + $cellLoc = clsTbsXmlCellReader::FindElement($rowLoc, $CellEl, $c_pos, true); + if ($cellLoc === false) { + if ($debug) echo "FAIL, rowLoc = " . $rowLoc->GetSrc(); + $currColOk = false; + } else { + if ($debug) echo "ok found"; + $c_pos = $cellLoc->PosEnd + 1; + $currCol++; + // Repeat info + $cellLoc->RepeatIdx = 1; + $cellLoc->RepeatMax = 1; + if ($AttCellR) { + $max = $cellLoc->GetAttLazy($AttCellR); + if ($max !== false) { + $cellLoc->RepeatMax = intval($max); + } + } + } + } + } + + // Insert missing cells + if ($debug) echo "\n * Insert Cell : check"; + if (!$currColOk) { + if ($AddMissing) { + // Insert the empty cells + $x = '<' . $CellEl . '>'; + $x_len = strlen($x); + $nb = ($targetCol - $currCol); + $rowLoc->AppendInnerSrc(str_repeat($x, $nb)); + // The cell locator must be targeted on the last inserted cell + $cellLoc = new clsTbsXmlCellReader($rowLoc->Txt, $CellEl, ($rowLoc->GetInnerAppendPos() - $x_len), null, $rowLoc, false); + $cellLoc->FindEndTag(); + } else { + // No more data => locator on a non-existing entity ($cellLoc->Exists = false) + if ($debug) echo "\n* Insert Cell : create phantom cell"; + $cellLoc = clsTbsXmlCellReader::CreatePhantomElement($rowLoc, $rowLoc->GetInnerAppendPos()); + } + $cellLoc->RepeatIdx = 1; + $cellLoc->RepeatMax = 1; + } + + $cellLoc->RowOk = $currRowOk; // true if the row did exists, false if the row has been added by the function + $cellLoc->cellRow = $targetRow; // row num in the sheet + $cellLoc->cellCol = $targetCol; // col num in the sheet + + // If the row is repeated, then we save its childs + if ( ($rowLoc->RepeatMax > 1) && ($rowLoc->RepeatIdx === 1) ) { + $rowLoc->CellLst[$targetCol] = $cellLoc; + } + + //echo "\n rowLoc->GetSrc = " . $rowLoc->GetSrc(); + //echo "\n cellLoc = " . var_export($cellLoc, true); + + if ($debug) echo "\n * Result cellLoc = ($targetRow, $targetCol) RowOk=".var_export($currRowOk,true).", Exists=".var_export($cellLoc->Exists,true); + if ($debug) echo "\n * cellLoc->GetSrc = " . $cellLoc->GetSrc(); + + // In case of a full colmun range, we don't return the extra missing columns. + // This means a valid row can have no cells. + if ( $Range['cfull'] && (!$cellLoc->Exists) ) { + if ($debug) echo " ... swicht to next cell"; + return $this->XML_GetNextCellLoc($SheetLoc, $Range, $cellLoc, $RowEl, $CellEl, $AttRowR, $AttCellR, $AddMissing); + } + + return $cellLoc; + + } + + /** + * Return the column number from a cell reference. First colmun is number 1. + * Can also return the row number if asked. + * Return 0 is the column is not specified. + * Return '' for the row num if it is not specified. + * + * @param string $CellRef The reference of a cell. Like "B3" or "AZ48". + * @param boolean $WithRow (optional) Use true in order to return both col and row numbers. + * + * @return integer|array|false The column number, or an array with both the colum number and the row number. + */ + function Sheet_ColNum($CellRef, $WithRow = false) { + + $col = 0; + $row = ''; + $rank = 0; + $Prefix = '$'; // character prefix allowed before col and row values. + + // We read the string backward because that the only way to know the rank. + for ($i = strlen($CellRef) -1 ; $i >= 0 ; $i--) { + $l = $CellRef[$i]; + if ($l === $Prefix) { + } elseif (is_numeric($l)) { + $row = $l . $row; // backwards + } else { + $l = ord(strtoupper($l)) -64; + if ($l>0 && $l<27) { + $col = $col + $l*pow(26,$rank); + } else { + return false; + } + $rank++; + } + } + + if ($WithRow) { + return array($col, intval($row)); + } else { + return $col; + } + + } + + /** + * Return the reference of the cell, such as 'A10'. + * @param integer $Col The column number (first is 1) + * @param integer $Row The row number (first is 1) + * @param string $Char (optional) The prefix for col and row num. + * @return string + */ + function Sheet_CellRef($Col, $Row, $Char = '') { + $r = ''; + $x = $Col; + do { + $x = $x - 1; + $c = ($x % 26); + $x = ($x - $c)/26; + $r = chr(65 + $c) . $r; // chr(65)='A' + } while ($x>0); + return ($Char . $r . $Char . $Row); + } + + /** + * Return the info of a range definition. + * Support both XLSX and ODS, range with or without sheet name, single cell range, full column range, full row range, absolute or relative cells, and multi-range. + * + * @param string $ref The range reference. + * + * XLSX : + * 'the_sheet_name'!$A1 + * 'the_sheet_name'!$A$1:$B$2 + * 'the_sheet_name'!$A$1:$B$2,'the_sheet_name2'!$A$1:$B$2 + * ODS : + * Sheet5.C9 + * $'the_sheet_name'.$A$1 + * $'the_sheet_name'.$A$1:.$B$2 + * $'the_sheet_name'.$A$1:'the_sheet_name'.$B$2 + * + * forbidden chars in sheet name : XLSX = ": ' [ ]" ; ODS = "[ ] * ? : / \ " or "'" as first char + * simple quotes are doubled + * the name can be delimited with "'" if there is any special char + * cell separator : XLSX = '!' ; ODS = "." (can be the first char if sheet name is ommited) + * range separator : XLSX = "," ; ODS = impossible + * + * @return array A recordset of info. + */ + function Sheet_GetRangeInfo($ref) { + + $delim = "'"; // Sheet name delimitor + $rsep = ","; // Range separator + + $result = array(); + + $i_end = strlen($ref) -1; + $new = true; + // I's easier to read backward since the range definition always ends with a cell reference + for ($i = $i_end ; $i >= 0 ; $i--) { + + // Initialize a new range info + if ($new) { + $sheet = ''; + $cells = ''; + $in_delim = false; // true if we are inside the delimited string + $is_cell = true; // true if wee reading the cell part + $is_xslx = false; // true if is seems to be an XLSX syntax + $new = false; // true if it is a new range defintion (XLSX ranges can be multi-range) + } + + // Read and interpret the char + $x = $ref[$i]; + + if ($is_cell) { + // We are reading the cell part + if ( ($x === '!') || ($x === '.') ) { + // We meet a cell separator + $is_cell = false; + $is_xslx = ($x === '!'); + } elseif ($x === $rsep) { + // We meet a range separator + $new = true; + } elseif ($x === $delim) { + // should never happen, but we can workaround this + $is_cell = false; + $sheet = $x; + } else { + $cells = $x . $cells; + } + } else { + // We are reading the sheet part + if ($in_delim) { + if ($x === $delim) { + if ( ($i > 0) && ($ref[$i -1] === $delim) ) { + // It's a double delim + $sheet = $x . $sheet; + $i--; + } else { + // It's a single delim. Note that the bound delim are not kept. + $in_delim = false; + } + } else { + $sheet = $x . $sheet; + } + } else { + if ($x === $delim) { + $in_delim = true; + } elseif ($x === ':') { + // case of ODS with two cells + $cells = $x . $cells; + $is_cell = true; + $sheet = ''; // the sheet name can be repeated in the second cell with Database Range + } elseif ($x === $rsep) { + $new = true; + } else { + $sheet = $x . $sheet; + } + } + } + + // Check for a new range info + if ( $new || ($i == 0) ) { + + // Clean up the sheet name + $sheet = trim($sheet, '$'); // ODS can ref can start with $ before the sheet name. + if (!$is_xslx) { + $sheet = htmlspecialchars_decode($sheet); // ODS only + } + + // pattern with all props + $info = array( + 'sheet' => $sheet, + 'cells' => $cells, + 'err' => false, + 'cs' => false, + 'rs' => false, + 'ce' => false, + 're' => false, + 'single' => false, + 'cfull' => false, + 'rfull' => false, + ); + + // Analyze the cells ref + $parts = explode(':', $cells); + foreach ($parts as $idx => $cell) { + $w = $this->Sheet_ColNum($cell, true); + if ($w === false) { + $info['err'] = "The range reference '{$cell}' is not recognized."; + $info['_ref'] = $ref; // for debuging + } else { + $ok = true; + if ($is_xslx) { + // we have to check that both col and row values have a $, otherwise the Excel syntaxe is not the same + // it is very curious : B8 => XFD1, C8 => A1, C$8 => A$8, $C8 => $C1 !!?? + if ($cell[0] !== '$') { + $ok = false; + } elseif (($w[0] !== 0) && ($w[1] !== 0) && (substr_count($cell, '$') != 2)) { + $ok = false; + } + } + if ($ok) { + $z = ($idx === 0) ? 's' : 'e'; + $info['c'.$z] = $w[0]; + $info['r'.$z] = $w[1]; + } else { + $info['err'] = "OpenTBS supports only absolute references in XLSX ranges."; + $info['_ref'] = $ref; // for debuging + } + } + } + + // For facilities, endings should be available + if ( ($info['ce'] === false) && ($info['re'] === false) ) { + $info['ce'] = $info['cs']; + $info['re'] = $info['rs']; + $info['single'] = true; + } + + // Set the full row or column info + if ($info['cs'] === 0) { + $info['cs'] = 1; + $info['ce'] = PHP_INT_MAX; + $info['cfull'] = true; + } + if ($info['re'] === 0) { + $info['rs'] = 1; + $info['re'] = PHP_INT_MAX; + $info['rfull'] = true; + } + + $result[] = $info; + + } + + } + + return $result; + + } + + /** + * Visit all the cells of a range for get or set. + * + */ + function Sheet_VisitCells($RangeRef, $Options, $Set) { + + // Retrieve options + if (!is_array($Options)) { + $Options = array(); + } + $opt_header = $this->getItem($Options, 'header', false); + $opt_noerr = $this->getItem($Options, 'noerr', false); + $opt_rangeinfo = $this->getItem($Options, 'rangeinfo', false); + $opt_columns = $this->getItem($Options, 'columns', false); + $opt_dbr = $this->getItem($Options, 'del_blank_rows', false); + $opt_row_max = $this->getItem($Options, 'row_max', false); + + // Get the type of contents + if ($this->ExtEquiv == 'ods') { + $isXlsx = false; + } elseif ($this->ExtEquiv == 'xlsx') { + $isXlsx = true; + } else { + // Not supported + return false; + } + + if ($isXlsx) { + $this->MsExcel_RangeNamesInit(); + } else { + $this->OpenDoc_RangeNamesInit(); + } + + // Get range information + if (isset($this->OtbsSheetRangeNames[$RangeRef])) { + // Named range + $x = $this->OtbsSheetRangeNames[$RangeRef]; + } else { + // Custom range definition + $x = $this->Sheet_GetRangeInfo($RangeRef); + } + + if (isset($x[0])) { + $Range = $x[0]; + if ($Range['err'] !== false) { + if ($opt_noerr) return false; + return $this->RaiseError("(VisitCells) The range reference '{$RangeRef}' cannot be found."); + } + } else { + if ($opt_noerr) return false; + return $this->RaiseError("(VisitCells) Unable to read the definition for the range named '{$RangeRef}'."); + } + if ($Range['err']) { + if ($opt_noerr) return false; + return $this->RaiseError("(VisitCells) Error for the range '{$RangeRef}' : " . $Range['err']); + } + + if ($opt_rangeinfo) { + return $Range; + } + + // Get sheet and range information + if ($isXlsx) { + $SheetLoc = $this->MsExcel_GetSheetLoc($Range); + } else { + $SheetLoc = $this->OpenDoc_GetSheetLoc($Range); + $this->OpenDoc_CoveredCells_Replace($SheetLoc, true); + } + if (!$SheetLoc) { + return false; + } + + //var_export($SheetLoc->GetSrc()); exit; + + $RowEl = ($isXlsx) ? 'row' : 'table:table-row'; + $CellEl = ($isXlsx) ? 'c' : 'table:table-cell'; + $AttRowR = ($isXlsx) ? false : 'table:number-rows-repeated'; + $AttCellR = ($isXlsx) ? false : 'table:number-columns-repeated'; + + // Visit all cells of the range + $ok = true; + $cell = false; + $result = array(); + $row_idx = -1; + $col_idx = -1; + // Manage header + $hdr_ok = false; // true if $hdr_lst is set + $hdr_lst = array(); + // Manage blank rows + $del_row = false; // Number of not empty values in the row + while ($ok && ($cell = $this->XML_GetNextCellLoc($SheetLoc, $Range, $cell, $RowEl, $CellEl, $AttRowR, $AttCellR, false)) ) { + if ($cell) { + + // Retrieve row and col indexes + $NewRow = ($cell->cellCol == $Range['cs']); + if ($NewRow) { + + // Manage header and colmun renaming + if ($row_idx === 0) { + if ($opt_header) { + $hdr_lst = $row; + $hdr_ok = true; + // Delete the row from the result + unset($result[0]); + $row_idx = -1; + $opt_header = false; // avoid a second pass + } + if (is_array($opt_columns)) { + if ($hdr_ok) { + $hdr_lst2 = array(); + foreach ($hdr_lst as $i => $n) { + if (isset($opt_columns[$i])) { + $hdr_lst2[$i] = $opt_columns[$i]; + } elseif (isset($opt_columns[$n])) { + $hdr_lst2[$i] = $opt_columns[$n]; + } + } + $hdr_lst = $hdr_lst2; + unset($hdr_lst2); + } else { + $hdr_ok = true; + $hdr_lst = $opt_columns; + } + $opt_columns = false; // avoid a second pass + } + } + + // The cell is the first of a row + $col_idx = 0; + unset($row); + if (!$del_row) $row_idx++; + $del_row = $opt_dbr; + + if ($opt_row_max !== false) { + if ($row_idx >= $opt_row_max) { + $ok = false; + } + } + + // Add a new empty row + if ($ok) { + $result[$row_idx] = array(); + $row =& $result[$row_idx]; + } + + } else { + $col_idx++; + } + + // Get column key and check if we read the value + if ($hdr_ok) { + if (isset($hdr_lst[$col_idx])) { + $col_key = $hdr_lst[$col_idx]; + } else { + $col_key = false; + } + } else { + $col_key = $col_idx; + } + + // Read the value + if ($ok && ($col_key !== false)) { + if ($isXlsx) { + $val = $this->MsExcel_GetCellValue($cell, $SheetLoc->xlsxFileIdx); + } else { + $val = $this->OpenDoc_GetCellValue($cell); + } + // Save the value + $row[$col_key] = $val; + // Manage blank rows + if ($del_row && (!is_null($val)) ) $del_row = false; + + } + + } else { + $ok = false; + } + } + + unset($row); + + // Delete the last inserted if needed + if ($del_row) { + unset($result[$row_idx]); + } + + return $result; + + } + + /** + * Return the extension of the file, lower case and without the dot. Example: 'png'. + */ + function Misc_FileExt($FileOrExt) { + $p = strrpos($FileOrExt, '.'); + $ext = ($p===false) ? $FileOrExt : substr($FileOrExt, $p+1); + $ext = strtolower($ext); + return $ext; + } + + /** + * Add or replace a credit information in the appropriate property of the document. + * Return the new credit text if succeed. + * Return false if the expected file is not found. + * @param string $NewCredit The text to set. + * @param boolean $Add Add the item. + * @param boolean $System Automatic system information. + * @param string $Type (optional) type of the item to add. + */ + function Misc_EditCredits($NewCredit, $Add, $System, $Type = null) { + + if ($this->ExtType=='odf') { + $File = 'meta.xml'; + $Tag = 'meta:user-defined'; + if (is_string($Type)) { + $n = $Type; + } else { + $n = ($System) ? 'Producer' : 'Creator'; + } + $Att = 'meta:name="' . $n .'"'; + $Parent = 'office:meta'; + } elseif ($this->ExtType=='openxml') { + $File = 'docProps/core.xml'; + $Tag = (is_string($Type)) ? $Type : 'dc:creator'; + $Att = false; + $Parent = 'cp:coreProperties'; + } else { + return false; + } + + $idx = $this->FileGetIdx($File); + if ($idx===false) return false; + + // prevent from XML injection + $NewCredit = htmlspecialchars($NewCredit, ENT_NOQUOTES); // ENT_NOQUOTES because target is an element's content + + $Txt = $this->TbsStoreGet($idx, "EditCredits"); + + if ($Att) { + $loc = clsTbsXmlLoc::FindElementHavingAtt($Txt, $Att, 0); + $TagOpen = $Tag.' '.$Att; + } else { + $loc = clsTbsXmlLoc::FindElement($Txt, $Tag, 0); + $TagOpen = $Tag; + } + + // On both OpenXML and ODF, the item must be unique. + if ($loc===false) { + $p = strpos($Txt, ''); + if ($p===false) return $p; + $Txt = substr_replace($Txt, '<'.$TagOpen.'>'.$NewCredit.'', $p, 0); + } else { + if ($Add) { + $NewCredit = $loc->GetInnerSrc().';'.$NewCredit; + } + $loc->ReplaceInnerSrc($NewCredit); + } + + $this->TbsStorePut($idx, $Txt); + + return $NewCredit; + + } + + /** + * Convert a string to an attribut’s value in OpenXML + */ + function OpenXML_AttVal($x) { + // Replace <>&" but not ' + return htmlspecialchars($x, ENT_COMPAT + ENT_SUBSTITUTE); + } + + /** + * Return the path of file $FullPath relatively to the path of file $RelativeTo. + * For example: + * 'dir1/dir2/file_a.xml' relatively to 'dir1/dir2/file_b.xml' is 'file_a.xml' + * 'dir1/file_a.xml' relatively to 'dir1/dir2/file_b.xml' is '../file_a.xml' + */ + function OpenXML_GetRelativePath($FullPath, $RelativeTo) { + + $fp = explode('/', $FullPath); + $fp_file = array_pop($fp); + $fp_max = count($fp)-1; + + $rt = explode('/', $RelativeTo); + $rt_file = array_pop($rt); + $rt_max = count($rt)-1; + + // First different item + $min = min($fp_max, $rt_max); + while( ($min>=0) && ($fp[0]==$rt[0]) ) { + $min--; + array_shift($fp); + array_shift($rt); + } + + $path = str_repeat('../', count($rt)); + $path .= implode('/', $fp); + if (count($fp)>0) $path .= '/'; + $path .= $fp_file; + + return $path; + + } + + /** + * Return the absolute path of file $RelativePath which is relative to the full path $RelativeTo. + * For example: + * '../file_a.xml' relatively to 'dir1/dir2/file_b.xml' is 'dir1/file_a.xml' + */ + function OpenXML_GetAbsolutePath($RelativePath, $RelativeTo) { + + // May be reltaive to the root + if (substr($RelativePath, 0, 1) == '/') { + return substr($RelativePath, 1); + } + + $rp = explode('/', $RelativePath); + $rt = explode('/', $RelativeTo); + + // Get off the file name; + array_pop($rt); + + while ($rp[0] == '..') { + array_pop($rt); + array_shift($rp); + } + + while ($rp[0] == '.') { + array_shift($rp); + } + + $path = array_merge($rt, $rp); + $path = implode('/', $path); + + return $path; + + } + + function OpenXML_GetMediaRelativeToCurrent() { + $file = $this->TBS->OtbsCurrFile; + $x = explode('/', $file); + $dir = $x[0] . '/media'; + return $this->OpenXML_GetRelativePath($dir, $file); + } + + /** + * Return the absolute internal path of a target for a given Rid used in the current file. + */ + function OpenXML_GetInternalPicPath($Rid) { + // $this->OpenXML_CTypesPrepareExt($InternalPicPath, ''); + $TargetDir = $this->OpenXML_GetMediaRelativeToCurrent(); + $o = $this->OpenXML_Rels_GetObj($this->TBS->OtbsCurrFile, $TargetDir); + if (isset($o->TargetLst[$Rid])) { + $x = $o->TargetLst[$Rid]; // relative path + return $this->OpenXML_GetAbsolutePath($x, $this->TBS->OtbsCurrFile); + } else { + return false; + } + } + + /** + * Delete an XML file in the OpenXML archive. + * The file is delete from the declaration file [Content_Types].xml and from the relationships of the specified files. + * @param {string} $FullPath The full path of the file to delete. + * @param {array} $RelatedTo List of the the full paths of the files than may have relationship with the file to delete. + * @return {mixed} False if it is not possible to delete the file, or the number of modifier relations ship in case of success (may be 0). + */ + function OpenXML_DeleteFile($FullPath, $RelatedTo) { + + // Delete the file in the archive + $idx = $this->FileGetIdx($FullPath); + if ($idx==false) return false; + $this->FileReplace($idx, false); + + // Delete the declaration of the file + $this->OpenXML_CTypesDeletePart('/' . $FullPath); + + // Delete the relationships + $nb = 0; + foreach ($RelatedTo as $file) { + $target = $this->OpenXML_GetRelativePath($FullPath, $file); + $att = 'Target="' . $target . '"'; + if ($this->OpenXML_Rels_DeleteRel($file, $att)) { + $nb++; + } + } + + return $nb; + + } + + /** + * Return the path of the Rel file in the archive for a given XML document. + * @param $DocPath Full path of the sub-file in the archive + */ + function OpenXML_Rels_GetPath($DocPath) { + $DocName = basename($DocPath); + return str_replace($DocName,'_rels/'.$DocName.'.rels',$DocPath); + } + + /** + * Delete an element in a Rels file. + * Take car that there is another technic for listing and adding targets wish is working with a persistent object which is commit at the end of the merge.. + * @param string $DocPath The fullpath of the document file. + * @param string $AttExpr The target att expression to find. + * @param string|boolean $ReturnAttLst The list of att values to return. + * @return mixed $ReturnAttVal (or True) if the change is applied. + */ + function OpenXML_Rels_DeleteRel($DocPath, $AttExpr, $ReturnAttLst = false) { + + $RelsPath = $this->OpenXML_Rels_GetPath($DocPath); + $idx = $this->FileGetIdx($RelsPath); + if ($idx===false) $this->RaiseError("Cannot edit target in '$RelsPath' because the file is not found."); + $txt = $this->TbsStoreGet($idx, 'Replace target in rels file'); + + $loc = clsTbsXmlLoc::FindElementHavingAtt($txt, $AttExpr, 0); + if ($loc) { + $ret = true; + if (is_array($ReturnAttLst)) { + $ret = array(); + foreach ($ReturnAttLst as $att) { + $ret[$att] = $loc->GetAttLazy($att); + } + } + $loc->Delete(); + $this->TbsStorePut($idx, $txt); + return $ret; + } else { + return false; + } + + } + + /** + * Return an object that represents the informations of an .rels file, but for optimization, targets are scanned only for asked directories. + * The result is stored in a cache so that a second call will not compute again. + * The function stores Rids of files existing in a the $TargetPrefix directory of the archive (image, ...). + * @param $DocPath Full path of the sub-file in the archive + * @param $TargetPrefix Prefix of the 'Target' attribute. For example $TargetPrefix='../drawings/' + */ + function OpenXML_Rels_GetObj($DocPath, $TargetPrefix) { + + if ($this->OpenXmlRid===false) $this->OpenXmlRid = array(); + + // Create the object if it does not exist yet + if (!isset($this->OpenXmlRid[$DocPath])) { + + $o = (object) null; + $o->RidLst = array(); // Current Rids in the template ($Target=>$Rid) + $o->TargetLst = array(); // Current Targets in the template ($Rid=>$Target) + $o->RidNew = array(); // New Rids to add at the end of the merge + $o->DirLst = array(); // Processed target dir + $o->ChartLst = false; // Chart list, computed in another method + + $o->FicPath = $this->OpenXML_Rels_GetPath($DocPath); + + $FicIdx = $this->FileGetIdx($o->FicPath); + if ($FicIdx===false) { + $o->FicType = 1; + $Txt = ''; + } else { + $o->FicIdx = $FicIdx; + $o->FicType = 0; + $Txt = $this->FileRead($FicIdx, true); + } + $o->FicTxt = $Txt; + $o->ParentIdx = $this->FileGetIdx($DocPath); + + $this->OpenXmlRid[$DocPath] = &$o; + + } else { + + $o = &$this->OpenXmlRid[$DocPath]; + $Txt = &$o->FicTxt; + + } + + // Feed the Rid and Target lists for the asked directory + if (!isset($o->DirLst[$TargetPrefix])) { + + $o->DirLst[$TargetPrefix] = true; + + // read existing Rid in the file + $zTarget = ' Target="'.$TargetPrefix; + $zId = ' Id="'; + $p = -1; + while (($p = strpos($Txt, $zTarget, $p+1))!==false) { + // Get the target name + $p1 = $p + strlen($zTarget); + $p2 = strpos($Txt, '"', $p1); + if ($p2===false) return $this->RaiseError("(OpenXML) end of attribute Target not found in position ".$p1." of sub-file ".$o->FicPath); + $TargetEnd = substr($Txt, $p1, $p2 -$p1); + $Target = $TargetPrefix.$TargetEnd; + // Get the Id + $p1 = strrpos(substr($Txt,0,$p), '<'); + if ($p1===false) return $this->RaiseError("(OpenXML) beginning of tag not found in position ".$p." of sub-file ".$o->FicPath); + $p1 = strpos($Txt, $zId, $p1); + if ($p1!==false) { + $p1 = $p1 + strlen($zId); + $p2 = strpos($Txt, '"', $p1); + if ($p2===false) return $this->RaiseError("(OpenXML) end of attribute Id not found in position ".$p1." of sub-file ".$o->FicPath); + $Rid = substr($Txt, $p1, $p2 - $p1); + $o->RidLst[$Target] = $Rid; + $o->TargetLst[$Rid] = $Target; + } + } + + } + + return $o; + + } + + /* + * Add a new Rid in the file in the Rels file. Return the Rid. + * Rels files are attached to XML files and are listing, and gives all rids and their corresponding targets used in the XML file. + */ + function OpenXML_Rels_AddNewRid($DocPath, $TargetDir, $FileName) { + + $o = $this->OpenXML_Rels_GetObj($DocPath, $TargetDir); + + $Target = $TargetDir.$FileName; + + if (isset($o->RidLst[$Target])) return $o->RidLst[$Target]; + + // Add the Rid in the information + $NewRid = 'opentbs'.(1+count($o->RidNew)); + $o->RidLst[$Target] = $NewRid; + $o->RidNew[$Target] = $NewRid; + + $this->IdxToCheck[$o->ParentIdx] = $o->FicIdx; + + return $NewRid; + + } + + // Save the changes in the rels files (works only for images for now) + function OpenXML_Rels_CommitNewRids ($Debug) { + + foreach ($this->OpenXmlRid as $doc => $o) { + + if (count($o->RidNew)>0) { + + // search position for insertion + $p = strpos($o->FicTxt, ''); + if ($p===false) return $this->RaiseError("(OpenXML) closing tag not found in subfile ".$o->FicPath); + + // build the string to insert + $x = ''; + foreach ($o->RidNew as $target=>$rid) { + $x .= ''; + } + + // insert + $o->FicTxt = substr_replace($o->FicTxt, $x, $p, 0); + + // save + if ($o->FicType==1) { + $this->FileAdd($o->FicPath, $o->FicTxt); + } else { + $this->FileReplace($o->FicIdx, $o->FicTxt); + } + + // debug mode + if ($Debug) $this->DebugLst[$o->FicPath] = $o->FicTxt; + + $this->OpenXmlRid[$doc]->RidNew = array(); // Erase the Rid done because there can be another commit + + } + } + + } + + /** + * Initialize modifications in '[Content_Types].xml'. + */ + function OpenXML_CTypesInit() { + if ($this->OpenXmlCTypes===false){ + $this->OpenXmlCTypes = array( + 'Extension'=>array(), + 'PartName'=>array() + ); + } + } + + /** + * Prepare information for adding a content type for an extension. + * It needs to be completed when a new picture file extension is added in the document. + */ + function OpenXML_CTypesPrepareExt($FileOrExt, $ct='') { + + $ext = $this->Misc_FileExt($FileOrExt); + + $this->OpenXML_CTypesInit(); + + $lst =& $this->OpenXmlCTypes['Extension']; + if (isset($lst[$ext]) && ($lst[$ext]!=='') ) return; + + if (($ct==='') && isset($this->ExtInfo['pic_ext'][$ext])) $ct = 'image/'.$this->ExtInfo['pic_ext'][$ext]; + + $lst[$ext] = $ct; + + } + + /** + * Delete a file in the declaration file. + * @param $PartName : path of the file to delete + */ + function OpenXML_CTypesDeletePart($PartName) { + $this->OpenXML_CTypesInit(); + $this->OpenXmlCTypes['PartName'][$PartName] = false; + } + + function OpenXML_CTypesCommit($Debug) { + + $file = '[Content_Types].xml'; + $idx = $this->FileGetIdx($file); + if ($idx===false) { + $Txt = ''; + } else { + $Txt = $this->FileRead($idx, true); + } + + $ok = false; + + // Delete PartNames + foreach ($this->OpenXmlCTypes['PartName'] as $part=>$val) { + if ($val===false) { + $loc = clsTbsXmlLoc::FindElementHavingAtt($Txt, 'PartName="'.$part.'"', 0); + if ($loc!==false) { + $loc->ReplaceSrc(''); + $ok = true; + } + } + } + + // Add missing extensions + $x = ''; + foreach ($this->OpenXmlCTypes['Extension'] as $ext=>$ct) { + $p = strpos($Txt, ' Extension="'.$ext.'"'); + if ($p===false) { + if ($ct==='') { + $this->RaiseError("(OpenXML) '".$ext."' is not an picture's extension recognize by OpenTBS."); + } else { + $x .= ''; + } + } + } + if ($x!=='') { + $p = strpos($Txt, ''); // search position for insertion + if ($p===false) return $this->RaiseError("(OpenXML) closing tag not found in subfile ".$file); + $Txt = substr_replace($Txt, $x, $p ,0); + $ok = true; + } + + + if ($ok) { + // debug mode + if ($Debug) $this->DebugLst[$file] = $Txt; + + if ($idx===false) { + $this->FileAdd($file, $Txt); + } else { + $this->FileReplace($idx, $Txt); + } + + } + + } + + function OpenXML_FirstPicType($Txt, $Pos, $Backward) { + // search the first image element in the given direction. Two types of image can be found. Return the value required for "att" parameter. + $TypeVml = '=$Pos) ) { + if ($t_curr===$TypeVml) { + // we take a new search for the next type of image + $t_curr = $TypeDml; + $p = -1; + } else { + $p = false; + } + } elseif ($p>$pMax) { + $pMax = $p; + $t = $t_curr; + } + } while ($p!==false); + } else { + $p1 = strpos($Txt, $TypeVml, $Pos); + $p2 = strpos($Txt, $TypeDml, $Pos); + if (($p1===false) && ($p2===false)) { + $t = ''; + } elseif ($p1===false) { + $t = $TypeDml; + } elseif ($p2===false) { + $t = $TypeVml; + } else { + $t = ($p1<$p2) ? $TypeVml : $TypeDml; + } + } + + if ($t===$TypeVml) { + return 'vml'; + } elseif ($t===$TypeDml) { + return 'dml'; + } else { + return false; + } + + } + + /** + * Build the OpenXmlMap property. It is the map of the types of sub-file in the document, according to the file [Content_Types].xml + * The structure is : [ short_type => [ list of sub-files ], ] + * Example : ['wordprocessingml.header+xml' => [ 'word/header1.xml', 'word/header2.xml', 'word/header2.xml' ] + */ + function OpenXML_MapInit() { + + $this->OpenXmlMap = array(); + $Map =& $this->OpenXmlMap; + + $file = '[Content_Types].xml'; + $idx = $this->FileGetIdx($file); + if ($idx===false) return; + + $Txt = $this->FileRead($idx, true); + + $type = ' ContentType="application/vnd.openxmlformats-officedocument.'; + $type_l = strlen($type); + $name = ' PartName="'; + $name_l = strlen($name); + + $p = -1; + while ( ($p=strpos($Txt, '<', $p+1))!==false) { + $pe = strpos($Txt, '>', $p); + if ($pe===false) return; // syntax error in the XML + $x = substr($Txt, $p+1, $pe-$p-1); + $pi = strpos($x, $type); + if ($pi!==false) { + $pi = $pi + $type_l; + $pc = strpos($x, '"', $pi); + if ($pc===false) return; // syntax error in the XML + $ShortType = substr($x, $pi, $pc-$pi); // content type's short value + $pi = strpos($x, $name); + if ($pi!==false) { + $pi = $pi + $name_l; + $pc = strpos($x, '"', $pi); + if ($pc===false) return; // syntax error in the XML + $Name = substr($x, $pi, $pc-$pi); // name + if ($Name[0]=='/') $Name = substr($Name,1); // fix the file path + if (!isset($Map[$ShortType])) $Map[$ShortType] = array(); + $Map[$ShortType][] = $Name; + } + } + $p = $pe; + } + + } + + /** + * Return the list of sub-files corresponding to one or several types. + * + * @param string|array $ShortTypes A short type, or a list of short types. + * + * @return array The list of the file names for all the asked types. + */ + function OpenXML_MapGetFiles($ShortTypes) { + if (is_string($ShortTypes)) $ShortTypes = array($ShortTypes); + $res = array(); + foreach ($ShortTypes as $type) { + if (isset($this->OpenXmlMap[$type])) { + $val = $this->OpenXmlMap[$type]; + foreach ($val as $file) $res[] = $file; + } + } + return $res; + } + + /** + * Return the first sub-file corresponding to a type. + * + * @param string $ShortType A short type. + * @param string $Default The default result id no sub-file is found. + * + * @return string The file name. + */ + function OpenXML_MapGetMain($ShortType, $Default) { + if (isset($this->OpenXmlMap[$ShortType])) { + return $this->OpenXmlMap[$ShortType][0]; + } else { + return $Default; + } + } + + /** + * Build the list of chart files. + */ + function OpenXML_ChartInit() { + + $this->OpenXmlCharts = array(); + + foreach ($this->CdFileByName as $f => $i) { + // Note : some of liste files are style or color files, not chart. + if (strpos($f, '/charts/')!==false) { + $x = explode('/',$f); + $n = count($x) -1; + if ( ($n>=2) && ($x[$n-1]==='charts') ) { + $x = $x[$n]; // name of the xml file + if (substr($x,-4)==='.xml') { + $x = substr($x,0,strlen($x)-4); + $this->OpenXmlCharts[$x] = array('idx'=>$i, 'parent_idx'=>false, 'series'=>false); + } + } + } + } + + } + + function OpenXML_ChartDebug($nl, $sep, $bull) { + + if ($this->OpenXmlCharts===false) $this->OpenXML_ChartInit(); + + echo $nl; + echo $nl."Charts technically stored in the document: (use command OPENTBS_CHART_INFO to get series's names and data)"; + echo $nl."------------------------------------------"; + + // list of supported charts + $nbr = 0; + foreach ($this->OpenXmlCharts as $key => $info) { + $ok = true; + if (!isset($info['series_nbr'])) { + $txt = $this->FileRead($info['idx'], true); + $info['series_nbr'] = substr_count($txt, ''); + $ok = (strpos($txt, '')!==false); + } + if ($ok) { + $nbr++; + echo $bull."name: '".$key."' , number of series: ".$info['series_nbr']; + } + } + + if ($this->TbsCurrIdx===false) { + echo $bull."(unable to scann more because no subfile is loaded)"; + } else { + $x = ' ProgID="MSGraph.Chart.'; + $x_len = strlen($x); + $p = 0; + $txt = $this->TBS->Source; + while (($p=strpos($txt, $x, $p))!==false) { + // check that the text is inside an xml tag + $p = $p + $x_len; + $p1 = strpos($txt, '>', $p); + $p2 = strpos($txt, '<', $p); + if ( ($p1!==false) && ($p2!==false) && ($p1<$p2) ) { + $nbr++; + $p1 = strpos($txt, '"', $p); + $z = substr($txt, $p, $p1-$p); + echo $bull."1 chart created using MsChart version ".$z." (series can't be merged with OpenTBS)"; + } + } + } + + if ($nbr==0) echo $bull."(none)"; + + } + + /** + * Search for the series in the chart definition + * @return mixed An Array if success, or a string if error. + */ + function OpenXML_ChartSeriesFound(&$Txt, $SeriesNameOrNum, $OnlyBounds) { + + if (is_numeric($SeriesNameOrNum)) { + $p = strpos($Txt, ''); // position of the series + if ($p===false) return "Number of the series not found."; + } else { + $SeriesNameOrNum = htmlspecialchars($SeriesNameOrNum, ENT_NOQUOTES); // ENT_NOQUOTES because target is an element's content + $p = strpos($Txt, '>'.$SeriesNameOrNum.'<'); + if ($p===false) return "Name of the series not found."; + $p++; + } + + $res = array('p'=>$p); + + $loc = clsTbsXmlLoc::FindElement($Txt, 'c:ser', $p, false); + if ($loc === false) { + return "XML entity not found."; + } + + $res['p'] = $loc->PosBeg; + $res['l'] = $loc->PosEnd - $loc->PosBeg + 1; + + if ($OnlyBounds) { + return $res; + } + + $src = substr($Txt, $res['p'], $res['l']); + + $this->OpenXML_ChartConvCacheToLiteral($src); + + // Legend, may be absent + $p = 0; + $p1 = strpos($src, ''); + if ($p1 > 0) { + $p2 = strpos($src, '', $p1); + $tag = ''; + $p1 = strpos($src, $tag, $p1); + if ( ($p1!==false) && ($p1<$p2) ) { + $p1 = $p1 + strlen($tag); + $p2 = strpos($src, '<', $p1); + $res['leg_p'] = $p1; + $res['leg_l'] = $p2 - $p1; + $p = $p2; + } + } + + // Data X & Y, we assume that (X or Category) are always first and (Y or Value) are always second + // Correspond elements are and or and + // Some charts may not have categories, they cannot be merged :-( + $type_num = 'c:numLit'; + $type_len = strlen($type_num); + for ($i = 1 ; $i <= 2 ; $i++) { + $p1 = strpos($src, ' or (the most common, means the source is a reference to a XLSX range), + // but also or if the source is literal (rare, means the source is given has is in the chart, I've seen it possible only in XSLX) + $p2 = strpos($src, 'Lit>', $p1); + /* no need if cache values have bee previously converted into literal + if ($p2 === false) { + $p2 = strpos($src, 'Cache>', $p1); + } + */ + if ($p2===false) return "Neither Literal nor Cached data is found for categories or values."; + $p2 = $p2 - $type_len + 1; // start of the closing tag + $type = substr($src, $p2 + 2, $type_len); // 'c:strLit' or 'c:numLit' + $res['coord_'.$i.'_p'] = $p1; + $res['coord_'.$i.'_l'] = $p2 - $p1; + $res['coord_'.$i.'_is_num'] = ($type == 'c:numLit'); + $p = $p2; + } + + $res['src'] = $src; + + return $res; + + } + + /** + * Find a chart in the template by its reference. + * Returns the OpenTBS's internal chart ref if found. + */ + function OpenXML_ChartFind($ChartRef, $ErrTitle) { + + if ($this->OpenXmlCharts===false) $this->OpenXML_ChartInit(); + + $ref = ''.$ChartRef; + + // try with $ChartRef as number + if (!isset($this->OpenXmlCharts[$ref])) { + $ref = 'chart'.$ref; + } + + // try with $ChartRef as name of the file + if (!isset($this->OpenXmlCharts[$ref])) { + + $charts = array(); + $fld = $this->OpenXML_AttVal('[' . $ChartRef . ']'); // tag to search in the Alt Text + $strLst = array( + ' title="'. $ChartRef . '"', // for compatibility, but since Office 2019 the title is not prposed anymore in the Alt Text perperties. + $fld, + ); + + // Find the subfile containing the frame + $idx = false; + if ($this->ExtEquiv=='pptx') { + // search in slides + $find = $this->MsPowerpoint_SearchInSlides($strLst, true, true); + $idx = $find['idx']; + } elseif ($this->ExtEquiv=='xlsx') { + // search in drawings + $find = $this->TbsSearchInFiles('xl/drawings/*.xml', $strLst, true, true); + $idx = $find['idx']; + } else { + $idx =$this->Ext_GetMainIdx(); + } + + // Get all charts in the subfile + if ($idx !== false) { + $charts = $this->OpenXML_ChartGetInfoFromFile($idx); + } + + // Search for the chart having the title + foreach($charts as $c) { + if ( ($c['title'] === $ChartRef) || (strpos('' . $c['descr'], $fld ) !== false) ) { + $ref = $c['name']; + } + } + + if (isset($this->OpenXmlCharts[$ref])) { + $chart = &$this->OpenXmlCharts[$ref]; + $this->OpenXmlCharts[$ChartRef] = &$chart; + // For debug + $chart['parent_idx'] = $idx; + } else { + return $this->RaiseError("($ErrTitle) : unable to find the chart corresponding to '".$ChartRef."'."); + } + } + + return $ref; + + } + + function OpenXML_ChartChangeSeries($ChartRef, $SeriesNameOrNum, $NewValues, $NewLegend=false) { + + // Search the chart + $ref = $this->OpenXML_ChartFind($ChartRef, 'ChartChangeSeries'); + if ($ref===false) return false; + + // Open the chart doc + $chart =& $this->OpenXmlCharts[$ref]; + $Txt = $this->TbsStoreGet($chart['idx'], 'ChartChangeSeries'); + if ($Txt===false) return false; + + $Delete = ($NewValues===false); + if (is_array($SeriesNameOrNum)) return $this->RaiseError("(ChartChangeSeries) '$ChartRef' : The series reference is an array, a string or a number is expected. ".$ChartRef."'."); // usual mistake in arguments + $ser = $this->OpenXML_ChartSeriesFound($Txt, $SeriesNameOrNum, $Delete); + if (!is_array($ser)) return $this->RaiseError("(ChartChangeSeries) '$ChartRef' : unable change series '".$SeriesNameOrNum."' in the chart '".$ref."' : ".$ser); + + if ($Delete) { + + $Txt = substr_replace($Txt, '', $ser['p'], $ser['l']); + + } else { + + $coord_1 = ''; // Categories + $coord_2 = ''; // Values + $i = 0; + $v = reset($NewValues); + if (is_array($v)) { + // syntax 2: $NewValues = array( array('cat1','cat2',...), array(val1,val2,...) ); + $k = key($NewValues); + $key_lst = &$NewValues[$k]; + $val_lst = &$NewValues[1]; + $simple = false; + } else { + // syntax 1: $NewValues = array('cat1'=>val1, 'cat2'=>val2, ...); + $key_lst = &$NewValues; + $val_lst = &$NewValues; + $simple = true; + } + foreach ($key_lst as $k=>$v) { + if ($simple) { + $x = $k; + $y = $v; + } else { + $x = $v; + $y = isset($val_lst[$k]) ? $val_lst[$k] : null; + } + // coord_1 can be numerical if the chart is a XY + $ok = (!is_null($x)) && ($x!==false) && ($x!=='') && ($y!=='NULL'); + // A category should not be missing otherwise its caption may not be display if the series is the first one + if ( $ok || (!$ser['coord_1_is_num']) ) { + // A non numerical value can produce a error while opening the document, without clue for debuging : « Word experienced an error trying to open the file ». + if ($ser['coord_1_is_num'] && (!is_numeric($x))) { + return $this->RaiseError("(ChartChangeSeries) '$ChartRef' : the value for X should be numerical. Provided value is : '$x'."); + } + $coord_1 .= ''.htmlspecialchars($x, ENT_NOQUOTES).''; + } + // But a missing value is supported by Ms Office. The idx attribute makes the association. + $ok = (!is_null($y)) && ($y!==false) && ($y!=='') && ($y!=='NULL'); + if ($ok) { + if ($ser['coord_2_is_num'] && (!is_numeric($y))) { + return $this->RaiseError("(ChartChangeSeries) '$ChartRef' : the value for the category '$x' should be numerical. Provided value is : '$y'."); + } + $coord_2 .= ''.htmlspecialchars($y, ENT_NOQUOTES).''; + } + $i++; + } + $coord_1 = ''.$coord_1; + $coord_2 = ''.$coord_2; // yes, the count is the same as coord_1 whenever missing values + + // change info in reverse order of placement in order to avoid extention problems + $src = $ser['src']; + unset($ser['src']); + $src = substr_replace($src, $coord_2, $ser['coord_2_p'], $ser['coord_2_l']); + $src = substr_replace($src, $coord_1, $ser['coord_1_p'], $ser['coord_1_l']); + if ( is_string($NewLegend) && isset($ser['leg_p']) && ($ser['leg_p'] < $ser['coord_1_p']) ) { + $NewLegend = htmlspecialchars($NewLegend, ENT_NOQUOTES); // ENT_NOQUOTES because target is an element's content + $src = substr_replace($src, $NewLegend, $ser['leg_p'], $ser['leg_l']); + } + + $Txt = substr_replace($Txt, $src, $ser['p'], $ser['l']); + + } + + $this->TbsStorePut($chart['idx'], $Txt, true); + + return true; + + } + + /** + * Return the list of all charts in the current sub-file, with title and description if any. + */ + function OpenXML_ChartGetInfoFromFile($idx, $Txt=false) { + + if ($idx===false) return false; + + $file = $this->CdFileLst[$idx]['v_name']; + $relative = (substr_count($file, '/')==1) ? '' : '../'; + $o = $this->OpenXML_Rels_GetObj($file, $relative.'charts/'); + + if ($o->ChartLst===false) { + + if ($Txt===false) $Txt = $this->TbsStoreGet($idx, 'OpenXML_ChartGetInfoFromFile'); + + $o->ChartLst = array(); + + $p = 0; + while ($t = clsTbsXmlLoc::FindStartTag($Txt, 'c:chart', $p)) { + $rid = $t->GetAttLazy('r:id'); + $name = false; + $title = false; + $descr = false; + // DOCX can embeds if inline with text, or otherwise + $parent = clsTbsXmlLoc::FindStartTag($Txt, 'w:drawing', $t->PosBeg, false); + if ($parent===false) { + // PPTX + $parent = clsTbsXmlLoc::FindStartTag($Txt, 'p:nvGraphicFramePr', $t->PosBeg, false); + } + if ($parent===false) { + // XLSX + $parent = clsTbsXmlLoc::FindStartTag($Txt, 'xdr:nvGraphicFramePr', $t->PosBeg, false); + } + if ($parent!==false) { + + $parent->FindEndTag(); + $src = $parent->GetInnerSrc(); + + // since Office 2019, attribute title is not avalaible + $el = clsTbsXmlLoc::FindStartTagHavingAtt($src, 'title', 0); + if ($el!==false) $title = $el->GetAttLazy('title'); + + // since Office 2019, attribute descr stands for Alt Text instead of Description. + $el = clsTbsXmlLoc::FindStartTagHavingAtt($src, 'descr', 0); + if ($el!==false) $descr = $el->GetAttLazy('descr'); + } + + if (isset($o->TargetLst[$rid])) { + $name = basename($o->TargetLst[$rid]); + if (substr($name,-4)==='.xml') $name = substr($name,0,strlen($name)-4); + } + $o->ChartLst[] = array('rid'=>$rid, 'title'=>$title, 'descr'=>$descr, 'name'=>$name); + $p = $t->PosEnd; + } + + } + + return $o->ChartLst; + + } + + /** + * Convert all cache values into literal values. + */ + function OpenXML_ChartConvCacheToLiteral(&$Txt) { + + // Unlink the data sheet by deleting references + $this->XML_DeleteElements($Txt, array('c:f')); + + $p = 0; + $ok = true; + while ( $ok && (($loc = clsTbsXmlLoc::FindElement($Txt, 'c:tx', $p)) !== false) ) { + // The title of the series can be , or + // We try to replace with , without touching to . + if ($loc_ref = clsTbsXmlLoc::FindElement($loc, 'c:strRef', 0)) { + if ($loc_v = clsTbsXmlLoc::FindElement($loc_ref, 'c:v', 0)) { + $src = $loc_v->getSrc(); + $loc->ReplaceInnerSrc($src); + } else { + $ok = false; // error met + } + } + $p = $loc->PosEnd; + } + + // Replace Reference values with Literal values + if ($ok) { + $this->XML_DeleteElements($Txt, array('c:numCache', 'c:strCache'), true); + $Txt = str_replace('c:strRef>', 'c:strLit>', $Txt); + $Txt = str_replace('c:numRef>', 'c:numLit>', $Txt); + } + + } + + /** + * Delete one, sveral or all categories in the chart. + * @param string $ChartRef The chart reference. + * @param string|array $del_categories An array of categories to delete, on the name of a category, all the keywork '*' that means all categories. + * @param boolean $no_err Indicate if an error is return when a searched category is not found. + * @return boolean Return true if all the searched categories are deleted. + */ + function OpenXML_ChartDelCategories($ChartRef, $del_categories, $no_err) { + + $nb_series = 0; + + // Search the chart + $ref = $this->OpenXML_ChartFind($ChartRef, 'ChartChangeSeries'); + if ($ref===false) return false; + + // Open the chart doc + $chart =& $this->OpenXmlCharts[$ref]; + $Txt = $this->TbsStoreGet($chart['idx'], 'ChartChangeSeries'); + if ($Txt===false) return false; + + // Prepare info for the search + $del_all = false; + if (is_string($del_categories)) { + if ($del_categories == '*') { + $del_all = true; + $del_categories = array(); + } else { + $del_categories = array($del_categories); + } + } + $del_categories = array_flip($del_categories); + + // global info + $glob_remain_cat = $del_categories; + $glob_nb_del = 0; + + $ps = 0; + while ($ser = clsTbsXmlLoc::FindElement($Txt, 'c:ser', $ps, true)) { + + $cat = clsTbsXmlLoc::FindElement($ser, 'c:cat', 0, true); + + // Scan all categories + $pc = 0; + $new_idx = array(); // associative list of array(old_idx => new_idx) + $del_nb = 0; + while ($cpt = clsTbsXmlLoc::FindElement($cat, 'c:pt', $pc, true)) { + // Note: a element can be missing if the is no data. So the true position is given by attribute idx. + $idx = intval($cpt->GetAttLazy('idx')); + $del = false; + $cv = clsTbsXmlLoc::FindElement($cpt, 'c:v', 0, true); + $category = $cv->GetInnerSrc(); + if ( $del_all || isset($del_categories[$category]) ) { + $cpt->Delete(); + $cpt->UpdateParent(true); + $del = true; + unset($glob_remain_cat[$category]); + } + if ($del) { + $del_nb++; + $glob_nb_del++; + $new_idx[$idx] = false; + // do not change $pc + } else { + $new_idx[$idx] = $idx - $del_nb; + if ($del_nb > 0) { + // The category to delete is found. Next categories must have they idx updated. + $idx = intval($cpt->GetAttLazy('idx')); + $cpt->ReplaceAtt('idx', $new_idx[$idx]); + $cpt->UpdateParent(true); + } + $pc = $cpt->PosEnd; + } + } + + if ($del_nb > 0) { + + // Update the count of categories. If not done then the chart displays an extra blank category. + if ($cnt = clsTbsXmlLoc::FindStartTag($cat, 'c:ptCount', 0, true)) { + $nb = intval($cnt->GetAttLazy('val')); + $cnt->ReplaceAtt('val', $nb - $del_nb); + $cnt->UpdateParent(true); + } + + // Delete the points that corresponf with the deleted categories + $del_val_nb = false; + $val = clsTbsXmlLoc::FindElement($ser, 'c:val', 0, true); // usually after + $pc = 0; + while ($cpt = clsTbsXmlLoc::FindElement($val, 'c:pt', $pc, true)) { + $idx = intval($cpt->GetAttLazy('idx')); + if ($new_idx[$idx] === false) { + $del_val_nb = true; + $cpt->Delete(); + $cpt->UpdateParent(true); + // do not change $pc + } elseif ($new_idx[$idx] === $idx) { + // no change + $pc = $cpt->PosEnd; + } else { + // change the index + $cpt->ReplaceAtt('idx',$new_idx[$idx]); + $cpt->UpdateParent(true); + $pc = $cpt->PosEnd; + } + } + + if ($del_val_nb > 0) { + // Update the count of values. If not done then the chart displays an extra blank category. + if ($cnt = clsTbsXmlLoc::FindStartTag($val, 'c:ptCount', 0, true)) { + $nb = intval($cnt->GetAttLazy('val')); + $cnt->ReplaceAtt('val', $nb - $del_val_nb); + $cnt->UpdateParent(true); + } + } + + } + + $ps = $ser->PosEnd; + + } + + // Save the file if modified + if ($glob_nb_del > 0) { + $this->TbsStorePut($chart['idx'], $Txt, true); + } + + // Result of the function + if ( $del_all || (count($glob_remain_cat) == 0) ) { + // All searched categories are deleted + return true; + } else { + if ($no_err) { + return false; + } else { + return $this->RaiseError("(ChartDelCategory) '$ChartRef' : unable to find categories '" . implode(', ', array_keys($glob_remain_cat)) . "' in the chart ".$ref."."); + } + } + + } + + /** + * Return information and adata about all series in the chart. + */ + function OpenXML_ChartReadSeries($ChartRef, $Complete) { + + // Search the chart + $ref = $this->OpenXML_ChartFind($ChartRef, 'ChartReadSerials'); + if ($ref===false) return false; + + // Open the chart doc + $chart =& $this->OpenXmlCharts[$ref]; + + $Txt = $this->TbsStoreGet($chart['idx'], 'ChartReadSerials'); + if ($Txt===false) return false; + + // Prepare loops + $serials = array(); + + $loop_conf = array( + 'names' => array('parent' => 'c:tx', 'format' => false), // name of the series + 'cat' => array('parent' => 'c:cat', 'format' => 'c:formatCode'), // categories of the series + 'val' => array('parent' => 'c:val', 'format' => 'c:formatCode'), // values of the series + ); + + // Loop + $loop_res = array(); + $ser_p = 0; + while ($ser_loc = clsTbsXmlLoc::FindElement($Txt, 'c:ser', $ser_p)) { + $res = array(); + foreach ($loop_conf as $key => $conf) { + if ($loc_parent = clsTbsXmlLoc::FindElement($ser_loc, $conf['parent'], 0)) { + // Search format + $format = false; + if ($conf['format']) { + if ($loc = clsTbsXmlLoc::FindElement($loc_parent, $conf['format'], 0)) { + $format = $loc->GetInnerSrc(); + $res[$key . '_format'] = $format; + } + } + // Search items. Works for both References or Literals values + // It is possible that a val item is missing for a cat idx + $items = array(); + $loc_p = 0; + while ($loc_pt = clsTbsXmlLoc::FindElement($loc_parent, 'c:pt', $loc_p)) { + $idx = $loc_pt->GetAttLazy('idx'); + $loc = clsTbsXmlLoc::FindElement($loc_pt, 'c:v', 0); + $items[$idx] = $loc->GetInnerSrc(); + $loc_p = $loc_pt->PosEnd; + } + $res[$key] = $items; + } else { + $res[$key] = false; + } + } + + // simplify name info + $names = $res['names']; + if (is_array($names) && isset($res['names'][0])) { + $res['name'] = $res['names'][0]; + } else { + $res['name'] = false; + } + if (is_array($names)) { + if (count($names) > 0) { + unset($res['names']); + } + } else { + unset($res['names']); + } + + $loop_res[] = $res; + $ser_p = $ser_loc->PosEnd; + } + + if ($Complete) { + return array( + 'file_idx' => $chart['idx'], + 'file_name' => $this->TbsGetFileName($chart['idx']), + 'parent_idx' => $chart['parent_idx'], + 'parent_name' => $this->TbsGetFileName($chart['parent_idx']), + 'series' => $loop_res, + ); + } else { + $series = array(); + foreach ($loop_res as $res) { + $series[$res['name']] = array($res['cat'], $res['val']); + } + return $series; + } + + return $loop_res; + + } + + function OpenXML_SharedStrings_Prepare() { + + $file = 'xl/sharedStrings.xml'; + $idx = $this->FileGetIdx($file); + if ($idx===false) return; + + $Txt = $this->TbsStoreGet($idx, 'Excel SharedStrings'); + if ($Txt===false) return false; + $this->TbsStorePut($idx, $Txt); // save for any further usage (is this usefull ??) + + $this->OpenXmlSharedStr = array(); + $this->OpenXmlSharedSrc =& $this->TbsStoreLst[$idx]['src']; + + } + + function OpenXML_SharedStrings_GetVal($id) { + // this function return the XML content of the string and put previous met values in cache + if ($this->OpenXmlSharedStr===false) $this->OpenXML_SharedStrings_Prepare(); + + $Txt =& $this->OpenXmlSharedSrc; + + if (!isset($this->OpenXmlSharedStr[$id])) { + $last_id = count($this->OpenXmlSharedStr) - 1; // last id in the cache + if ($last_id<0) { + $p2 = 0; // no items found yet + } else { + $p2 = $this->OpenXmlSharedStr[$last_id]['end']; + } + $x1 = 'RaiseError("(Excel SharedStrings) id $id is searched but id $last_id is not found."); + $p1 = strpos($Txt, '>', $p1+$x1_len)+1; + $p2 = strpos($Txt, $x2, $p1); + if ($p2===false) return $this->RaiseError("(Excel SharedStrings) id $id is searched but no closing tag found for id $last_id."); + $this->OpenXmlSharedStr[$last_id] = array('beg'=>$p1, 'end'=>$p2, 'len'=>($p2-$p1)); + } + } + + $str =& $this->OpenXmlSharedStr[$id]; + + return substr($Txt, $str['beg'], $str['len']); + + } + + // Delete unreferenced images + function OpenMXL_GarbageCollector() { + + if ( (count($this->IdxToCheck)==0) && (count($this->OtbsSheetSlidesDelete)==0) ) return; + + // Key for Pictures + $pic_path = $this->ExtInfo['pic_path']; + $pic_path_len = strlen($pic_path); + + // Key for Rels + $rels_ext = '.rels'; + $rels_ext_len = strlen($rels_ext); + + // List all Pictures and Rels files + $pictures = array(); + $rels = array(); + foreach ($this->CdFileLst as $idx=>$f) { + $n = $f['v_name']; + if (substr($n, 0, $pic_path_len)==$pic_path) { + $short = basename($pic_path).'/'.basename($n); + $pictures[] = array('name'=>$n, 'idx'=>$idx, 'nbr'=>0, 'short'=>$short); + } elseif (substr($n, -$rels_ext_len)==$rels_ext) { + if ($this->FileGetState($idx)!='d') $rels[$n] = $idx; + } + } + + // Read contents or Rels files + foreach ($rels as $n=>$idx) { + $txt = $this->TbsStoreGet($idx, 'GarbageCollector'); + foreach ($pictures as $i=>$info) { + if (strpos($txt, $info['short'].'"')!==false) $pictures[$i]['nbr']++; + } + } + + // Delete unused Picture files + foreach ($pictures as $info) { + if ($info['nbr']==0) $this->FileReplace($info['idx'], false); + } + + + } + + /** + * MsWord cut the source of the text when a modification is done. This is splitting TBS tags. + * This function repare the split text by searching and delete duplicated layout. + * + * @param string $Txt (by reference) the XML source to modify + * @param string $tagR tag name of a Run element + * @param string $tagT tag name of a Text element + * + * @return integer the number of deleted dublicates. + */ + function OpenMXL_CleanDuplicatedLayout(&$Txt, $tagR, $tagT) { + + $wro = '<' . $tagR; + $wro_len = strlen($wro); + + $wrc = 'XML_SearchTagForward($Txt, $wro, $wro_p)) !== false ) { // next tag + $wto_p = $this->XML_SearchTagForward($Txt, $wto, $wro_p); // next tag + if ($wto_p === false) return false; // error in the structure of the element + $first = true; + $nb = 0; // number of duplicated layouts for the current text snippet + do { + $ok = false; + $wtc_p = $this->XML_SearchTagForward($Txt, $wtc, $wto_p); // next tag + if ($wtc_p === false) return false; + $wrc_p = $this->XML_SearchTagForward($Txt, $wrc, $wro_p); // next tag (only to check inclusion) + if ($wrc_p === false) return false; + if ( ($wto_p < $wrc_p) && ($wtc_p < $wrc_p) ) { // if the is actually included in the element + if ($first) { + // we build the xml that would be the duplicated layout if any + $p = strpos($Txt, '<', $wrc_p + $wrc_len); + $x = substr($Txt, $wtc_p, $p - $wtc_p); // ' ' may include some linebreaks or spaces after the closing tags + $src_to_del = $x . substr($Txt, $wro_p, ($wto_p + $wto_len) - $wro_p); // without the last symbol, like: '....', '', $src_to_del); // tabs must not be deleted between parts => they nt be in the superfluous string + $src_to_del_len = strlen($src_to_del); + $first = false; + } + // if the following source is a duplicated layout then we delete it by joining the elements. + $p_att = $wtc_p + $src_to_del_len; + $x = substr($Txt, $p_att, 1); // help to optimize the check because if it's a duplicated layout, the char after is the end of the '')) && (substr($Txt, $wtc_p, $src_to_del_len)===$src_to_del) ) { + $p_end = strpos($Txt, '>', $p_att); // + if ($p_end === false) return false; // error in the structure of the tag + $Txt = substr_replace($Txt, '', $wtc_p, $p_end - $wtc_p + 1); // delete superfluous part + attributes + $nb_tot++; + $nb++; + $ok = true; + } + } + } while ($ok); + + // Add or delete the attribute { xml:space="preserve" } that must be set if there is a space before of after the text + if ($nb > 0) { + $with_space = false; + if ( substr($Txt, $wtc_p - 1, 1) === ' ') $with_space = true; + $p_end = strpos($Txt, '>', $wto_p); // first char of the text + if ( substr($Txt, $p_end + 1, 1) === ' ') $with_space = true; + $src = substr($Txt, $wto_p, $p_end - $wto_p + 1); + $p = strpos($src, $preserve); + if ( $with_space && ($p === false) ) { + // add the attribute + $Txt = substr_replace($Txt, ' ' . $preserve, $p_end, 0); + } elseif ( (!$with_space) && ($p !== false) ) { + // delete the attribute + $Txt = substr_replace($Txt, '', $wto_p + $p -1, $preserve_len + 1); // delete the attribut with the space before it + } + } + + $wro_p = $wro_p + $wro_len; + + } + + return $nb_tot; // number of total replacements + + } + + function MsExcel_ConvertToRelative(&$Txt) { + // attribute "r" is optional since missing row are added using + // attribute "r" is optional since missing cells are added using + $Loc = new clsTbsLocator; + $this->MsExcel_ConvertToRelative_Item($Txt, $Loc, 'row', 'r', true); + } + + function MsExcel_ConvertToRelative_Item(&$Txt, &$Loc, $Tag, $Att, $IsRow) { + // convert tags $Tag which have a position (defined with attribute $Att) into relatives tags without attribute $Att. Missing tags are added as empty tags. + $item_num = 0; + $tag_len = strlen($Tag); + $missing = '<'.$Tag.'/>'; + $closing = ''; + $p = 0; + $compat_limit_miss = 1000; + $compat_limit_num = 1048576 - 10000; + while (($p=clsTinyButStrong::f_Xml_FindTagStart($Txt, $Tag, true, $p, true, true))!==false) { + + $Loc->PrmPos = array(); + $Loc->PrmLst = array(); + $p2 = $p + $tag_len + 2; // count the char '<' before and the char ' ' after + $PosEnd = strpos($Txt, '>', $p2); + clsTinyButStrong::f_Loc_PrmRead($Txt,$p2,true,'\'"','<','>',$Loc, $PosEnd, true); // read parameters + $Delete = false; + if (isset($Loc->PrmPos[$Att])) { + // attribute found + $a = $Loc->PrmLst[$Att]; + if ($IsRow) { + $r = intval($a); + } else { + $r = $this->Sheet_ColNum($a); + if ($r === false) { + return $this->RaiseError('(ConvertToRelative) Reference of cell \'' . $a . '\' cannot be recognized.'); + } + } + $missing_nbr = $r - $item_num -1; + if ($missing_nbr<0) { + return $this->RaiseError('(Excel Consistency) error in counting items <'.$Tag.'>, found number '.$r.', previous was '.$item_num); + } elseif($IsRow && ($missing_nbr > $compat_limit_miss) && ($r >= $compat_limit_num)) { // Excel limit is 1048576 + // Useless final rows: LibreOffice add several final useless rows in the sheet when saving as XLSX. + $Delete = true; + $item_num++; + } else { + // delete the $Att attribute + $pp = $Loc->PrmPos[$Att]; + $pp[3]--; //while ($Txt[$pp[3]]===' ') $pp[3]--; // external end of the attribute, may has an extra spaces + $x_p = $pp[0]-1; // we take out the space + $x_len = $pp[3] - $x_p +1; + $Txt = substr_replace($Txt, '', $x_p, $x_len); + $PosEnd = $PosEnd - $x_len; + // If it's a cell, we look if it's a good idea to replace the shared string + if ( (!$IsRow) && isset($Loc->PrmPos['t']) && ($Loc->PrmLst['t']==='s') ) $this->MsExcel_ReplaceString($Txt, $p, $PosEnd); + // add missing items before the current item + if ($missing_nbr>0) { + $x = str_repeat($missing, $missing_nbr); + $x_len = strlen($x); + $Txt = substr_replace($Txt, $x, $p, 0); + $PosEnd = $PosEnd + $x_len; + $x = ''; // empty the memory + } + $item_num = $r; + } + } else { + // nothing to change the item is already relative + $item_num++; + } + if ($Delete) { + if (($Txt[$PosEnd-1]!=='/')) { + $x_p = strpos($Txt, $closing, $PosEnd); + if ($x_p===false) return $this->RaiseError('(Excel Consistency) closing row tag is not found.'); + $PosEnd = $x_p + strlen($closing) - 1; + } + $Txt = substr_replace($Txt, '', $p, $PosEnd - $p + 1); + } elseif ($IsRow && ($Txt[$PosEnd-1]!=='/')) { + // It's a row item that may contain columns + $x_p = strpos($Txt, $closing, $PosEnd); + if ($x_p===false) return $this->RaiseError('(Excel Consistency) closing row tag is not found.'); + $x_len0 = $x_p - $PosEnd -1; + $x = substr($Txt, $PosEnd+1, $x_len0); + $this->MsExcel_ConvertToRelative_Item($x, $Loc, 'c', 'r', false); + $Txt = substr_replace($Txt, $x, $PosEnd+1, $x_len0); + $x_len = strlen($x); + $p = $x_p + $x_len - $x_len0; + } else { + $p = $PosEnd; + } + } + + } + + /** + * Add the attribute in all and items, and delete empty items. + */ + function MsExcel_ConvertToExplicit(&$Txt) { + if (strpos($Txt, '')===false) return; + $this->MsExcel_ConvertToExplicit_Item($Txt, 'row', 'r', false); + } + + /** + * Add the attribute that gives the reference of the item. + * Return the number of inserted attributes. + * Note: substr() and strpos() function's execution time are geometrically increasing with then string length. + * So it is for this function. converting a sheet with 5.000 rows may have a duration of 15 sec. + */ + function MsExcel_ConvertToExplicit_Item(&$Txt, $Tag, $Att, $ParentRowNum) { + + $tag_pc = strlen($Tag) + 1; + $rpl = '<'.$Tag.' '.$Att.'="'; + $rpl_len = strlen($rpl); + $rpl_nbr = 0; + $item_num = 0; + + $p = clsTinyButStrong::f_Xml_FindTagStart($Txt, $Tag, true, 0, true, true); + if ($p === false) return; + + if ($p === 0) { + $Txt_Done = ''; + } else { + $Txt_Done = substr($Txt, 0, $p); + $Txt = substr($Txt, $p); + } + + do { + + // Next item + $p_next = clsTinyButStrong::f_Xml_FindTagStart($Txt, $Tag, true, 0 + $tag_pc, true, true); + + // Small text containing the current item + if ($p_next === false) { + $Txt_Curr = $Txt; + $Txt = ''; + } else { + $Txt_Curr = substr($Txt, 0, $p_next); + $Txt = substr($Txt, $p_next); + } + + $item_num++; + + if (substr($Txt_Curr, 0 + $tag_pc, 1) == '/') { + + // It's an empty item => Delete the item + $Txt_Done .= substr($Txt_Curr, 0 + $tag_pc + 2); // +2 is for the tail '/>' + + } else { + + // The item is not empty => replace attribute and delete the previous empty item in the same time + $ref = ($ParentRowNum===false) ? $item_num : $this->Sheet_CellRef($item_num, $ParentRowNum); + $Txt_Curr = $rpl . $ref . '"' . substr($Txt_Curr, 0 + $tag_pc); + $rpl_nbr++; + + // If it's a row => search for cells + if ($ParentRowNum===false) { + $nbr = $this->MsExcel_ConvertToExplicit_Item($Txt_Curr, 'c', 'r', $item_num); + } + + $Txt_Done .= $Txt_Curr; + + } + + } while ($p_next !== false); + + $Txt = $Txt_Done . $Txt; + + return $rpl_nbr; + + } + + /** + * Cells with formulas also have a cached result (stored in the element). + * When Excel open a sheet, the cached result is displayed, not the actualized result. + * We can add attribute ca="true" to the element in order to have them actualized. + * But this des not work well when the XLSX is opened with LibreOffice. + * So the solid solution is to simply delete the cached result. + * + * Cached values are stored in an array so it can be retrived for Sheet_VisitCells() + */ + function MsExcel_DeleteFormulaResults($idx, &$Txt) { + + if (!isset($this->MsExcel_Formulas[$idx])) { + $this->MsExcel_Formulas[$idx] = array(); + } + $formulas =& $this->MsExcel_Formulas[$idx]; + + $p = 0; + while ( ($locF = clsTbsXmlLoc::FindElement($Txt, 'f', $p, true)) !== false ) { + + $f = $locF->GetInnerSrc(); + + // Since 2018, Office 365 brings dynamic array formulas. They can be typed array even if they are single, and they have a "ref" attribute + // that can makes the XLSX invalid if the ref does not start with ref of the cell. Unfortunately this can happen when cells are duplicated with OpenTBS. + // In order to avoid invalid XML, then if it is a dynamic array formula but on a single cell, then we turn it into a simple formula. + $t = $locF->GetAttLazy('t'); + if ($t == 'array') { + $ref = $locF->GetAttLazy('ref'); + if (strpos($ref, ':') === false) { + $locF->ReplaceSrc('' . $f . ''); + } + } + + $p = $locF->PosEnd; + + $v = null; + if ($locC = clsTbsXmlLoc::FindElement($Txt, 'c', $locF->PosBeg, false)) { + if ($locV = clsTbsXmlLoc::FindElement($locC, 'v', 0, true)) { + $v = $locV->GetInnerSrc(); + $locV->Delete(); + $locV->UpdateParent(true); + } + $p = $locC->PosEnd; + } + $formulas[$f] = $v; + + } + + } + + /** + * XLSX has a file that refers to formulas in the entire workbook in order to schedule the calculations. + * The cells references in this file mey become erroneous since cell has been deleted or added in some sheets. + * Hopefully this file is optional. We have to deleted it. + */ + function MsExcel_DeleteCalcChain() { + return $this->OpenXML_DeleteFile('xl/calcChain.xml', array('xl/workbook.xml')); + } + + function MsExcel_ReplaceString(&$Txt, $p, &$PosEnd) { + // replace a SharedString into an InlineStr only if the string contains a TBS field + static $c = ''; + static $v1 = ''; + static $v1_len = 3; + static $v2 = ''; + static $v2_len = 4; + + // found position of the element, and extract its contents + $p_close = strpos($Txt, $c, $PosEnd); + if ($p_close===false) return; + $x_len = $p_close - $p; + $x = substr($Txt, $p, $x_len); // [ ... ] + + // found position of the element, and extract its contents + $v1_p = strpos($x, $v1); + if ($v1_p==false) return false; + $v2_p = strpos($x, $v2, $v1_p); + if ($v2_p==false) return false; + $vt = substr($x, $v1_p+$v1_len, $v2_p - $v1_p - $v1_len); + + // extract the SharedString id, and retrieve the corresponding text + $v = intval($vt); + if (($v==0) && ($vt!='0')) return false; + if (isset($this->MsExcel_NoTBS[$v])) return true; + $s = $this->OpenXML_SharedStrings_GetVal($v); + + // if the SharedSring has no TBS field, then we save the id in a list of known id, and we leave the function + if (strpos($s, $this->TBS->_ChrOpen)===false) { + $this->MsExcel_NoTBS[$v] = true; + return true; + } + + // prepare the replace + $x1 = substr($x, 0, $v1_p); + $x3 = substr($x, $v2_p + $v2_len); + $x2 = ''.$s.''; + $x = str_replace(' t="s"', ' t="inlineStr"', $x1).$x2.$x3; + + $Txt = substr_replace($Txt, $x, $p, $x_len); + + $PosEnd = $p + strlen($x); // $PosEnd is used to search the next item, so we update it + + } + + function MsExcel_ChangeCellType(&$Txt, &$Loc, $Ope) { + // change the type of a cell in an XLSX file + + $Loc->PrmLst['cellok'] = $Ope; // avoid the field to be processed twice + + if ( ($Ope==='xlsxString') || ($Ope==='tbs:string')) return true; + + static $OpeLst = array( + 'tbs:bool'=>' t="b"', + 'xlsxBool'=>' t="b"', + 'xlsxDate'=>'', + 'xlsxNum'=>'', + 'tbs:date'=>'', + 'tbs:num'=>'', + // compatibility with ODF format + 'tbs:time'=>'', + 'tbs:percent'=>'', + 'tbs:curr'=>'', + ); + + if (!isset($OpeLst[$Ope])) return false; + + $t0 = clsTinyButStrong::f_Xml_FindTagStart($Txt, 'c', true, $Loc->PosBeg, false, true); + if ($t0===false) return false; // error in the XML structure + + $te = strpos($Txt, '>', $t0); + if ( ($te===false) || ($te>$Loc->PosBeg) ) return false; // error in the XML structure + + $len = $te - $t0 + 1; + $c_open = substr($Txt, $t0, $len); // '' + $c_open = str_replace(' t="inlineStr"', $OpeLst[$Ope], $c_open); + + $t1 = strpos($Txt, '', $te); + if ($t1===false) return false; // error in the XML structure + + $p_is1 = strpos($Txt, '', $te); + if (($p_is1===false) || ($p_is1>$t1) ) return false; // error in the XML structure + + $is2 = ''; + $p_is2 = strpos($Txt, $is2, $p_is1); + if (($p_is2===false) || ($p_is2>$t1) ) return false; // error in the XML structure + $p_is2 = $p_is2 + strlen($is2); // move to end the of the tag + + $middle_len = $p_is1 - $te - 1; + $middle = substr($Txt, $te + 1, $middle_len); // text bewteen and + + // new tag to replace ... + static $v = '[]'; + $v_len = strlen($v); + $v_pos = strpos($v, '[]'); + + $x = $c_open.$middle.$v; + + $Txt = substr_replace($Txt, $x, $t0, $p_is2 - $t0); + + // move the TBS field + $p_fld = $t0 + strlen($c_open) + $middle_len + $v_pos; + $Loc->PosBeg = $p_fld; + $Loc->PosEnd = $p_fld +1; + + } + + function MsExcel_ChangeCellValue(&$Loc, &$Value) { + + switch ($Loc->PrmLst['cellok']) { + case 'tbs:num': + case 'tbs:curr': + case 'tbs:percent': + case 'xlsxNum': + if (is_numeric($Value)) { + // we have to check contents in order to avoid Excel errors. Note that value '0.00000000000000' makes an Excel error. + if (strpos($Value,'e')!==false) { // exponential representation + $Value = var_export((float) $Value, true); // this string conversion is not affected by the decimal separator given by the locale setting + } elseif (strpos($Value,'x')!==false) { // hexa representation + $Value = '' . hexdec($Value); + } elseif (strpos($Value,'.')===false) { + // it is better to not convert because of big numbers + // intval(7580563123) returns -1009371469 in 32bits + } else { + $Value = var_export((float) $Value, true); + } + } else { + $Value = ''; + } + break; + case 'tbs:bool': + case 'xlsxBool': + $Value = ($Value) ? 1 : 0; + break; + case 'tbs:date': + case 'tbs:time': + case 'xlsxDate': + if (is_string($Value)) { + $t = strtotime($Value); // We look if it's a date + } else { + $t = $Value; + } + if (($t===-1) || ($t===false)|| ($t===null)) { // Date not recognized + $Value = ''; + } elseif ($t===943916400) { // Date to zero + $Value = ''; + } else { // It's a date + $Value = ($t/86400.00)+25569; // unix: 1 means 01/01/1970, xlsx: 1 means 01/01/1900 + } + break; + default: + // do nothing + } + } + + function MsExcel_SheetInit() { + + if ($this->MsExcel_Sheets!==false) return; + + $this->MsExcel_Sheets = array(); // sheet info sorted by location + + $idx = $this->FileGetIdx('xl/workbook.xml'); + $this->MsExcel_Sheets_WkbIdx = $idx; + if ($idx===false) return; + + $Txt = $this->TbsStoreGet($idx, 'SheetInfo'); // use the store, so the file will be available for editing if needed + if ($Txt===false) return false; + $this->TbsStorePut($idx, $Txt); + + // scann sheet list + $p = 0; + $i = 0; + $rels = array(); + while ($loc=clsTbsXmlLoc::FindStartTag($Txt, 'sheet', $p, true) ) { + $o = (object) null; + $o->num = $i + 1; + // SheetId is not the numbered sheet in the workbook. It may have a missing sheet id. + $o->sheetId = $loc->GetAttLazy('sheetId'); + $o->rid = $loc->GetAttLazy('r:id'); + $o->name = $loc->GetAttLazy('name'); + $o->state = $loc->GetAttLazy('state'); + $o->stateR = ($o->state===false) ? 'visible' : $o->state; + $o->file = false; + $this->MsExcel_Sheets[$i] = $o; + $rels[$o->rid] =& $this->MsExcel_Sheets[$i]; + $i++; + $p = $loc->PosEnd; + } + + // Retrieve Sheet files + $idx = $this->FileGetIdx('xl/_rels/workbook.xml.rels'); + $Txt = $this->FileRead($idx); + if ($Txt===false) return false; + + $p = 0; + while ($loc=clsTbsXmlLoc::FindStartTag($Txt, 'Relationship', $p, true) ) { + $rid = $loc->GetAttLazy('Id'); + if (isset($rels[$rid])) $rels[$rid]->file = $loc->GetAttLazy('Target'); + $p = $loc->PosEnd; + } + + } + + /** + * Return the sheet info corresponding to the name, number or internal id. + * @param string|integer $IdOrName + * @param array $SearchBy A list of search condition, in order. Supported items : 'name' , 'sheetId', 'num' + * @param boolean $RaiseError Set true if an error is raised if the sheet is not found. + * @param object A special object. See MsExcel_SheetInit(). + */ + function MsExcel_SheetGetConf($IdOrName, $SearchBy, $RaiseError) { + + $this->MsExcel_SheetInit(); + + $by_name = in_array('name', $SearchBy, true); + $by_sheetId = in_array('sheetId', $SearchBy, true); + $by_num = in_array('num', $SearchBy, true); + + foreach($this->MsExcel_Sheets as $o) { + // Check by order of search. + foreach ($SearchBy as $s) { + $ok = false; + $ok = $ok || ( ($s === 'name') && ($o->name == $IdOrName) ); + $ok = $ok || ( ($s === 'sheetId') && ($o->sheetId == $IdOrName) ); + $ok = $ok || ( ($s === 'num') && ($o->num == $IdOrName) ); + if ($ok) { + return $o; + } + } + } + + if ($RaiseError) { + return $this->RaiseError("(MsExcel_SheetInit) The sheet '$IdOrName' is not found inside the Workbook. Try command OPENTBS_DEBUG_INFO to check all sheets inside the current Workbook."); + } else { + return false; + } + + } + + /** + * Check if the file name is a subfile corresponding to a sheet. + */ + function MsExcel_SheetIsIt($FileName) { + $this->MsExcel_SheetInit(); + foreach($this->MsExcel_Sheets as $o) { + if ($FileName=='xl/'.$o->file) return true; + } + return false; + } + + function MsExcel_SheetDebug($nl, $sep, $bull) { + + $this->MsExcel_SheetInit(); + + echo $nl; + echo $nl."Sheets in the Workbook:"; + echo $nl."-----------------------"; + foreach ($this->MsExcel_Sheets as $o) { + $name = str_replace(array('&','"','<','>'), array('&','"','<','>'), $o->name); + echo $bull."num: ".$o->num.", id: ".$o->sheetId.", name: [".$name."], state: ".$o->stateR.", file: xl/".$o->file; + } + + } + + // Actually delete, display of hide sheet marked for this operations. + function MsExcel_SheetDeleteAndDisplay() { + + if ( (count($this->OtbsSheetSlidesDelete)==0) && (count($this->OtbsSheetSlidesVisible)==0) ) return; + + $this->MsExcel_SheetInit(); + + $WkbTxt = $this->TbsStoreGet($this->MsExcel_Sheets_WkbIdx, 'Sheet Delete and Display'); + $nothing = false; + + $change = false; + $refToDel = array(); + + // process sheet in reverse order of their positions + foreach ($this->MsExcel_Sheets as $o) { + $zid = 'i:'.$o->num; + $zname = 'n:'.$o->name; // the value in the name attribute is XML protected + if ( isset($this->OtbsSheetSlidesDelete[$zname]) || isset($this->OtbsSheetSlidesDelete[$zid]) ) { + // Delete the sheet + $this->MsExcel_DeleteSheetFile($o->file, $o->rid, $WkbTxt); + $change = true; + $ref1 = str_replace(array('"','\''), array('"','\'\''), $o->name); + $ref2 = "'".$ref1."'"; + $refToDel[] = $ref1; + $refToDel[] = $ref2; + unset($this->OtbsSheetSlidesDelete[$zname]); + unset($this->OtbsSheetSlidesDelete[$zid]); + unset($this->OtbsSheetSlidesVisible[$zname]); + unset($this->OtbsSheetSlidesVisible[$zid]); + } elseif ( isset($this->OtbsSheetSlidesVisible[$zname]) || isset($this->OtbsSheetSlidesVisible[$zid]) ) { + // Hide or display the sheet + $visible = (isset($this->OtbsSheetSlidesVisible[$zname])) ? $this->OtbsSheetSlidesVisible[$zname] : $this->OtbsSheetSlidesVisible[$zid]; + $state = ($visible) ? 'visible' : 'hidden'; + if ($o->stateR!=$state) { + if (!$visible) $change = true; + $loc = clsTbsXmlLoc::FindStartTagHavingAtt($WkbTxt, 'r:id="'.$o->rid.'"', 0); + if ($loc!==false) $loc->ReplaceAtt('state', $state, true); + } + unset($this->OtbsSheetSlidesVisible[$zname]); + unset($this->OtbsSheetSlidesVisible[$zid]); + } + } + + // If they are deleted or hidden sheet, then it could be the active sheet, so we delete the active tab information + // Note: activeTab attribute seems to not be a sheet id, but rather a tab id. + if ($change) { + $loc = clsTbsXmlLoc::FindStartTag($WkbTxt, 'workbookView', 0); + if ($loc!==false) $loc->DeleteAtt('activeTab'); + } + + // Delete name of cells () that refer to a deleted sheet + foreach ($refToDel as $ref) { + // The name of the sheets is used in the reference, but with small changes + $p = 0; + while ( ($p = strpos($WkbTxt, '>'.$ref.'!', $p)) !==false ) { + $p2 = strpos($WkbTxt, '>', $p+1); + $p1 = strrpos(substr($WkbTxt, 0, $p), '<'); + if ( ($p1!==false) && ($p2!==false) ) { + $WkbTxt = substr_replace($WkbTxt, '', $p1, $p2 - $p1 +1); + } else { + $p++; + } + } + // + } + + // can make Excel error, no problem with + $WkbTxt = str_replace('', '', $WkbTxt); + + // store the result + $this->TbsStorePut($this->MsExcel_Sheets_WkbIdx, $WkbTxt); + + $this->TbsSheetCheck(); + + } + + function MsExcel_DeleteSheetFile($file, $rid, &$WkbTxt) { + + $this->OpenXML_DeleteFile('xl/' . $file, array('xl/workbook.xml')); + + // Delete in workbook.xml + if ($rid!=false) { + $loc = clsTbsXmlLoc::FindElementHavingAtt($WkbTxt, 'r:id="'.$rid.'"', 0); + if ($loc!==false) $loc->ReplaceSrc(''); + } + + } + + // Return the list of images in the current sheet + function MsExcel_GetDrawingLst() { + + $lst = array(); + + $dir = '../drawings/'; + $dir_len = strlen($dir); + $o = $this->OpenXML_Rels_GetObj($this->TBS->OtbsCurrFile, $dir); + foreach($o->TargetLst as $t) { + if ( (substr($t, 0, $dir_len)===$dir) && (substr($t, -4)==='.xml') ) $lst[] = 'xl/drawings/'.substr($t, $dir_len); + } + + return $lst; + } + + function MsExcel_RangeNamesInit() { + + if ($this->OtbsSheetRangeNames !== false) return; + + $this->OtbsSheetRangeNames = array(); + + // Get the workbook.xml contents + $idx = $this->FileGetIdx('xl/workbook.xml'); + if ($idx===false) return; + $Txt = $this->TbsStoreGet($idx, 'RangeNamesInit'); // use the store, so the file will be available for editing if needed + if ($Txt===false) return false; + //$this->TbsStorePut($idx, $Txt); + + $p = 0; + while ( $el = clsTbsXmlLoc::FindElement($Txt, 'definedName', $p, true) ) { + $name = $el->GetAttLazy('name'); // forbidden in range name : ", ', ' ', + $ref = $el->GetInnerSrc(); + $this->OtbsSheetRangeNames[$name] = $this->Sheet_GetRangeInfo($ref); + $p = $el->PosEnd; + } + + } + + /** + * Get the locator of the sheet element. + * @param array $Range The range information. + * @return object A clsTbsXmlLoc locator of false if error. + */ + function MsExcel_GetSheetLoc($Range) { + + // Get the sheet content + if ($Range['sheet']) { + $o = $this->MsExcel_SheetGetConf($Range['sheet'], array('name'), true); + if ($o === false) return false; + $idx = $this->FileGetIdx('xl/'.$o->file); + $Txt = $this->TbsStoreGet($idx, 'MsExcel_GetSheetLoc'); + if ($this->LastReadNotStored) { + $this->MsExcel_ConvertToRelative($Txt); + } + } else { + $Txt = $this->TBS->Source; + $idx = $this->TbsCurrIdx; + } + + $SheetLoc = clsTbsXmlLoc::FindElement($Txt, 'sheetData', 0, true); + $SheetLoc->xlsxFileIdx = $idx; + return $SheetLoc; + + } + + function MsExcel_GetCellValue($Loc, $fileIdx = -1) { + + $x = null; + + if ( $Loc->Exists && ($Loc->GetInnerStart() !== false) ) { + $type = $Loc->GetAttLazy('t'); + $vtag = ($type === 'inlineStr') ? 't' : 'v'; + if ($locV = clsTbsXmlLoc::FindElement($Loc, $vtag, 0, true)) { + $v = $locV->GetInnerSrc(); + } else { + $v = false; + if ($locF = clsTbsXmlLoc::FindElement($Loc, 'f', 0, true)) { + if (isset($this->MsExcel_Formulas[$fileIdx])) { + $f = $locF->GetInnerSrc(); + if (isset($this->MsExcel_Formulas[$fileIdx][$f])) { + $v = $this->MsExcel_Formulas[$fileIdx][$f]; + } + } + if ($v === false) { + $x = "#OpenTBS: formula without cached result"; + } + } else { + // it's valid to have no value + } + } + if ($v !== false) { + switch ($type) { + case 'b': // boolean: 0=false + $x = (boolean) $v; break; + case 's': // shared string + $x = $this->OpenXML_SharedStrings_GetVal($v); + $this->XML_DeleteElements($x, array('t'), true); + break; + case 'inlineStr': // inline string + $x = $v; break; + case 'str': // formula returning a string + $x = $v; break; + case 'd': // date + $t = ($v-25569.0) * 86400.0; // unix: 1 means 01/01/1970, xlsx: 1 means 01/01/1900 + $x = date('Y-m-d h:i:s', $t); + break; + case 'e': // error, example of value: #DIV/0! + $x = $v; break; + default: // false or 'n' => it's a number + if (strpos($v, '.') === false) { + $x = intval($v); + } else { + $x = floatval($v); + } + } + } + } + + return $x; + + } + + /** + * Return the list of slides in the Ms Powerpoint presentation. + * @param {boolean} $Master Trye to operate on master slides. + * @return {array} The list of the slides, of false if an error occurs. + */ + function MsPowerpoint_InitSlideLst($Master = false) { + + if ($Master) { + $RefLst = &$this->OpenXmlSlideMasterLst; + } else { + $RefLst = &$this->OpenXmlSlideLst; + } + + if ($RefLst!==false) return $RefLst; + + $PresFile = 'ppt/presentation.xml'; + + $prefix = ($Master) ? 'slideMasters/' : 'slides/'; + $o = $this->OpenXML_Rels_GetObj('ppt/presentation.xml', $prefix); + + $Txt = $this->FileRead($PresFile); + if ($Txt===false) return false; + + $p = 0; + $i = 0; + $lst = array(); + $tag = ($Master) ? 'p:sldMasterId' : 'p:sldId'; + while ($loc = clsTbsXmlLoc::FindStartTag($Txt, $tag, $p)) { + $i++; + $rid = $loc->GetAttLazy('r:id'); + if ($rid===false) { + $this->RaiseError("(Init Slide List) attribute 'r:id' is missing for slide #$i in '$PresFile'."); + } elseif (isset($o->TargetLst[$rid])) { + $f = 'ppt/'.$o->TargetLst[$rid]; + $lst[] = array('file' => $f, 'idx' => $this->FileGetIdx($f), 'rid' => $rid); + } else { + $this->RaiseError("(Init Slide List) Slide corresponding to rid=$rid is not found in the Rels file of '$PresFile'."); + } + $p = $loc->PosEnd; + } + + $RefLst = $lst; + return $RefLst; + + } + + // Clean tags in an Ms Powerpoint slide + function MsPowerpoint_Clean(&$Txt) { + + // Simplify Run Properties elements + $this->MsPowerpoint_CleanRpr($Txt, 'a:rPr'); + $Txt = str_replace('', '', $Txt); + + $this->MsPowerpoint_CleanRpr($Txt, 'a:endParaRPr'); + $Txt = str_replace('', '', $Txt); // do not delete, can change layout + + // Join split elements + $Txt = str_replace('', '', $Txt); + $Txt = str_replace('', '', $Txt); // this join TBS split tags + + // Delete empty elements + // An must contain at least one . An empty may exist after several merges or an OpenTBS cleans. + $Txt = str_replace('', '', $Txt); + + $this->OpenMXL_CleanDuplicatedLayout($Txt, 'a:r', 'a:t'); + + } + + /** + * Simplfy elements by deleting useless attributes + */ + function MsPowerpoint_CleanRpr(&$Txt, $elem) { + $p = 0; + while ($x = clsTbsXmlLoc::FindStartTag($Txt, $elem, $p)) { + $x->DeleteAtt('noProof'); + $x->DeleteAtt('lang'); + $x->DeleteAtt('err'); + $x->DeleteAtt('smtClean'); + $x->DeleteAtt('dirty'); + $p = $x->PosEnd; + } + } + + /** + * Search a string in all slides of the Presentation. + */ + function MsPowerpoint_SearchInSlides($str, $any, $returnFirstFound) { + + // init the list of slides + $this->MsPowerpoint_InitSlideLst(); // List of slides + + // build the list of files in the expected structure + $files = array(); + foreach($this->OpenXmlSlideLst as $i=>$s) $files[$i+1] = $s['idx']; + + // search + $find = $this->TbsSearchInFiles($files, $str, $any, $returnFirstFound); + + return $find; + + } + + function MsPowerpoint_SlideDebug($nl, $sep, $bull) { + + $this->MsPowerpoint_InitSlideLst(); // List of slides + + echo $nl; + echo $nl.count($this->OpenXmlSlideLst)." slide(s) in the Presentation:"; + echo $nl."-------------------------------"; + foreach ($this->OpenXmlSlideLst as $i => $s) { + echo $bull."#".($i+1).": ".basename($s['file']).", file: " . $s['file']; + } + if (count($this->OpenXmlSlideLst)==0) echo $bull."(none)"; + + // List of charts + echo $nl; + echo $nl."Charts found in slides:"; + echo $nl."-------------------------"; + + $nbr = 0; + for ($s=1; $s <= count($this->OpenXmlSlideLst); $s++) { + $this->OnCommand(OPENTBS_SELECT_SLIDE, $s); + $ChartLst = $this->OpenXML_ChartGetInfoFromFile($this->TbsCurrIdx); + foreach ($ChartLst as $i=>$c) { + $name = ($c['name']===false) ? '(not found)' : $c['name']; + $title = ($c['title']===false) ? '(not found)' : var_export($c['title'], true); + echo $bull."slide: $s, chart name: '$name', title: $title"; + if ($c['descr']!==false) echo ", description: ".$c['descr']; + $nbr++; + } + } + if ($nbr==0) echo $bull."(none)"; + + } + + // Actually delete slides in the Presentation + function MsPowerpoint_SlideDelete() { + + if ( (count($this->OtbsSheetSlidesDelete)==0) && (count($this->OtbsSheetSlidesVisible)==0) ) return; + + $this->MsPowerpoint_InitSlideLst(); + + // Edit both XML and REL of file 'presentation.xml' + + $xml_file = 'ppt/presentation.xml'; + $xml_idx = $this->FileGetIdx($xml_file); + $rel_idx = $this->FileGetIdx($this->OpenXML_Rels_GetPath($xml_file)); + + $xml_txt = $this->TbsStoreGet($xml_idx, 'Slide Delete and Display / XML'); + $rel_txt = $this->TbsStoreGet($rel_idx, 'Slide Delete and Display / REL'); + + $del_lst = array(); + $del_lst2 = array(); + $first_kept = false; // Name of the first slide, to be kept + foreach ($this->OpenXmlSlideLst as $i=>$s) { + $ref = 'i:'.($i+1); + if (isset($this->OtbsSheetSlidesDelete[$ref]) && $this->OtbsSheetSlidesDelete[$ref] ) { + + // the rid may be used several time in the fiel. i.e.: in , but also in + while ( ($x = clsTbsXmlLoc::FindElementHavingAtt($xml_txt, 'r:id="'.$s['rid'].'"', 0))!==false ) { + $x->ReplaceSrc(''); // delete the element + } + + $x = clsTbsXmlLoc::FindElementHavingAtt($rel_txt, 'Id="'.$s['rid'].'"', 0); + if ($x!==false) $x->ReplaceSrc(''); // delete the element + + $del_lst[] = $s['file']; + $del_lst[] = $this->OpenXML_Rels_GetPath($s['file']); + $del_lst2[] = basename($s['file']); + + } else { + $first_kept = basename($s['file']); + } + } + + $this->TbsStorePut($xml_idx, $xml_txt); + $this->TbsStorePut($rel_idx, $rel_txt); + unset($xml_txt, $rel_txt); + + // Delete references in '[Content_Types].xml' + foreach ($del_lst as $f) { + $this->OpenXML_CTypesDeletePart('/'.$f); + } + + // Change references in 'viewProps.xml.rels' + $idx = $this->FileGetIdx('ppt/_rels/viewProps.xml.rels'); + $txt = $this->TbsStoreGet($idx, 'Slide Delete and Display / viewProps'); + $ok = false; + foreach ($del_lst2 as $f) { + $z = 'Target="slides/'.$f.'"'; + if (strpos($txt, $z)) { + if ($first_kept===false) return $this->RaiseError("(Slide Delete and Display) : no slide left to replace the default slide in 'viewProps.xml.rels'."); + $ok = true; + $txt = str_replace($z, 'Target="slides/'.$first_kept.'"' , $txt); + } + } + if ($ok) $this->TbsStorePut($idx, $txt); + + // Actually delete the slide files + foreach ($del_lst as $f) { + $idx = $this->FileGetIdx($f); + unset($this->TbsStoreLst[$idx]); // delete the slide from the merging if any + $this->FileReplace($idx, false); + } + + } + + /** + * Return true if the file name is a slide. + */ + function MsPowerpoint_SlideIsIt($FileName) { + $this->MsPowerpoint_InitSlideLst(); + foreach ($this->OpenXmlSlideLst as $i => $s) { + if ($FileName==$s['file']) return true; + } + return false; + } + + // Cleaning tags in MsWord + function MsWord_Clean(&$Txt) { + $Txt = str_replace('', '', $Txt); // faster + //$this->MsWord_CleanFallbacks($Txt); + $this->XML_DeleteElements($Txt, array('w:proofErr', 'w:noProof', 'w:lang', 'w:lastRenderedPageBreak')); + $this->MsWord_CleanSystemBookmarks($Txt); + $this->MsWord_CleanRsID($Txt); + $this->OpenMXL_CleanDuplicatedLayout($Txt, 'w:r', 'w:t'); + } + + /** + * entities may contains duplicated TBS fields and this may corrupt the merging. + * This function delete such entities if they seems to contain TBS fields. This make the DOCX content less compatible with previous Word versions. + * https://wiki.openoffice.org/wiki/OOXML/Markup_Compatibility_and_Extensibility + */ + function MsWord_CleanFallbacks(&$Txt) { + + $p = 0; + $nb = 0; + while ( ($loc = clsTbsXmlLoc::FindElement($Txt,'mc:Fallback',$p))!==false ) { + if (strpos($loc->GetSrc(), $this->TBS->_ChrOpen) !== false ) { + $loc->Delete(); + $nb++; + } + $p = $loc->PosEnd; + } + + } + + function MsWord_CleanSystemBookmarks(&$Txt) { + // Delete GoBack hidden bookmarks that appear since Office 2010. Example: + + $x = ' w:name="_GoBack"/>', $p + $x_len); + if ($pe===false) return false; + $pb = strrpos(substr($Txt,0,$p) , '<'); + if ($pb===false) return false; + if (substr($Txt, $pb, $b_len)===$b) { + $Txt = substr_replace($Txt, '', $pb, $pe - $pb + 1); + $p = $pb; + $nbr_del++; + } else { + $p = $pe +1; + } + } + + return $nbr_del; + + } + + /** + * Delete XML attributes relative to log of user modifications. Returns the number of deleted attributes. + * In order to insert such information, MsWord does split TBS tags with XML elements. + * After such attributes are deleted, we can concatenate duplicated XML elements. + */ + function MsWord_CleanRsID(&$Txt) { + + $rs_lst = array('w:rsidR', 'w:rsidRPr'); + + $nbr_del = 0; + foreach ($rs_lst as $rs) { + + $rs_att = ' '.$rs.'="'; + $rs_len = strlen($rs_att); + + $p = 0; + while ($p!==false) { + // search the attribute + $ok = false; + $p = strpos($Txt, $rs_att, $p); + if ($p!==false) { + // attribute found, now seach tag bounds + $po = strpos($Txt, '<', $p); + $pc = strpos($Txt, '>', $p); + if ( ($pc!==false) && ($po!==false) && ($pc<$po) ) { // means that the attribute is actually inside a tag + $p2 = strpos($Txt, '"', $p+$rs_len); // position of the delimiter that closes the attribute's value + if ( ($p2!==false) && ($p2<$pc) ) { + // delete the attribute + $Txt = substr_replace($Txt, '', $p, $p2 -$p +1); + $ok = true; + $nbr_del++; + } + } + if (!$ok) $p = $p + $rs_len; + } + } + + } + + // delete empty tags + $Txt = str_replace('', '', $Txt); + $Txt = str_replace('', '', $Txt); + + return $nbr_del; + + } + + /** + * Prevent from the problem of missing spaces when calling ->MsWord_CleanRsID() or under certain merging circumstances. + * Replace attribute xml:space="preserve" used in , with the same attribute in . + * This trick use an attribute of element that works for all MsWord versions (last tested is 2019) but is undocumented. + * So it may may be disabled by default in a next version. + * LibreOffice does ignore this attribute in . + */ + function MsWord_CleanSpacePreserve(&$Txt) { + + $XmlLoc = clsTbsXmlLoc::FindStartTag($Txt, 'w:document', 0); + + // Checks + if ($XmlLoc===false) return; + if ($XmlLoc->GetAttLazy('xml:space') === 'preserve') return; + + // We delete the attribute on elements. This is not not mendatory but cleanner and save space. + // Canceled because needed for LibreOffice + // $Txt = str_replace(' xml:space="preserve"', '', $Txt); + + $XmlLoc->ReplaceAtt('xml:space', 'preserve', true); + + } + + /** + * Renumber attribute "id " of elements in order to ensure unicity. + * Such elements are used in objects. + */ + function MsWord_RenumDocPr(&$Txt) { + + $this->MsWord_DocPrId; + + $el = '', $p); + if ($pc!==false) { + $x = substr($Txt, $p, $pc - $p); + $pi = strpos($x, $id); + if ($pi!==false) { + $pi = $pi + $id_len; + $pq = strpos($x, '"', $pi); + if ($pq!==false) { + $i_len = $pq - $pi; + $i = intval(substr($x, $pi, $i_len)); + if ($i>0) { // id="0" is erroneous + if ($i > $this->MsWord_DocPrId) { + $this->MsWord_DocPrId = $i; // nothing else to do + } else { + $this->MsWord_DocPrId++; + $id_txt = '' . $this->MsWord_DocPrId; + $Txt = substr_replace($Txt, $id_txt, $p + $pi, $i_len); + $nbr++; + } + } + } + } + } + } + } + + return $nbr; + + } + + // Alias of block: 'tbs:page' + function MsWord_GetPage($Tag, $Txt, $Pos, $Forward, $LevelStop) { + + // Search the two possible tags for having a page-break + $loc1 = clsTbsXmlLoc::FindStartTagHavingAtt($Txt, 'w:type="page"', $Pos, $Forward); + $loc2 = clsTbsXmlLoc::FindStartTag($Txt, 'w:pageBreakBefore', $Pos, $Forward); + + // Define the position of start for the corresponding paragraph + if ( ($loc1===false) && ($loc2===false) ) { + if ($Forward) { + // End of the last paragraph of the document. + // The elements can be embeded, and it can be a single tag if it cnotains no text. + $loc = clsTbsXmlLoc::FindElement($Txt, 'w:p', strlen($Txt), false); + if ($loc===false) return false; + return $loc->PosEnd; + } else { + // start of the first paragraph of the document + $loc = clsTbsXmlLoc::FindStartTag($Txt, 'w:p', 0, true); + if ($loc===false) return false; + return $loc->PosBeg; + } + } + + // Take care that elements can be sef-embeded. + // That's why we assume that there is no page-break in an embeded paragraph while it is useless but possible. + if ($loc1===false) { + $s = $loc2->PosBeg; + } elseif($loc2===false) { + $s = $loc1->PosBeg; + } else { + if ($Forward) { + $s = ($loc1->PosBeg < $loc2->PosBeg) ? $loc1->PosBeg : $loc2->PosBeg; + } else { + $s = ($loc1->PosBeg > $loc2->PosBeg) ? $loc1->PosBeg : $loc2->PosBeg; + } + } + $loc = clsTbsXmlLoc::FindStartTag($Txt, 'w:p', $s, false); + + $p = $loc->PosBeg; + if ($Forward) $p--; // if it's forward, we stop the block before the paragraph with page-break + return $p; + + } + + // Alias of block: 'tbs:draw' + function MsWord_GetDraw($Tag, $Txt, $Pos, $Forward, $LevelStop) { + + $tags = array('mc:AlternateContent', 'w:r'); + + $p = $Pos; + + if ($Forward) { + foreach ($tags as $tag) { + $p = strpos($Txt, '', $p); + if ($p === false) return false; + } + } else { + foreach ($tags as $tag) { + $loc = clsTbsXmlLoc::FindStartTag($Txt, $tag, $p, false); + if ($loc === false) return false; + $p = $loc->PosBeg; + } + } + + return $p; + + } + + /** + * Alias of block: 'tbs:section' + * In Docx, section-breaks can be saved in the last of the section, or just after the last of the section. + * In practice, there is always at least one sectin-break and only the last section-break is saved outside the . + */ + function MsWord_GetSection($Tag, $Txt, $Pos, $Forward, $LevelStop) { + + // First we check if the TBS tag is inside a and if this has a + $case = false; + $locP = clsTbsXmlLoc::FindStartTag($Txt, 'w:p', $Pos, false); + if ($locP!==false) { + $locP->FindEndTag(true); + if ($locP->PosEnd>$Pos) { + $src = $locP->GetSrc(); + $loc = clsTbsXmlLoc::FindStartTag($src, 'w:sectPr', 0, true); + if ($loc!==false) $case = true; + } + } + + if ($case && $Forward) return $locP->PosEnd; + + // Look for the next section-break + $p = ($Forward) ? $locP->PosEnd : $locP->PosBeg; + $locS = clsTbsXmlLoc::FindStartTag($Txt, 'w:sectPr', $p, $Forward); + + if ($locS===false) { + if ($Forward) { + // end of the body + $p = strpos($Txt, '', $Pos); + return ($p===false) ? false : $p - 1; + } else { + // start of the body + $loc2 = clsTbsXmlLoc::FindStartTag($Txt, 'w:body', 0, true); + return ($loc2===false) ? false : $loc2->PosEnd + 1; + } + } + + // is inside a ? + $ewp = ''; + $inside = false; + $p = strpos($Txt, $ewp, $locS->PosBeg); + if ($p!==false) { + $loc2 = clsTbsXmlLoc::FindStartTag($Txt, 'w:p', $locS->PosBeg, true); + if ( ($loc2===false) || ($loc2->PosBeg>$p) ) $inside = true; + } + + $offset = ($Forward) ? 0 : 1; + if ($inside) { + return $p + strlen($ewp) - 1 + $offset; + } else { + // not inside + $locS->FindEndTag(); + return $locS->PosEnd + $offset; + } + + } + + /** + * Initialize information about header and footer files + */ + function MsWord_InitHeaderFooter() { + + if ($this->MsWord_HeaderFooter!==false) return; + + $types_ok = array('default' => true, 'first' => false, 'even' => false); + + // Is there a different header/footer for odd an even pages ? + $idx = $this->FileGetIdx('word/settings.xml'); + if ($idx!==false) { + $Txt = $this->TbsStoreGet($idx, 'GetHeaderFooterFile'); + $types_ok['even'] = (strpos($Txt, '')!==false); + unset($Txt); + } + + // Is there a different header/footer for the first page ? + $idx = $this->FileGetIdx('word/document.xml'); + if ($idx===false) return false; + $Txt = $this->TbsStoreGet($idx, 'GetHeaderFooterFile'); + $types_ok['first'] = (strpos($Txt, '')!==false); + + $places = array('header', 'footer'); + $files = array(); + $rels = $this->OpenXML_Rels_GetObj('word/document.xml', ''); + + foreach ($places as $place) { + $p = 0; + $entity = 'w:' . $place . 'Reference'; + while ($loc = clsTbsXmlLoc::FindStartTag($Txt, $entity, $p)) { + $p = $loc->PosEnd; + $type = $loc->GetAttLazy('w:type'); + if (isset($types_ok[$type]) && $types_ok[$type]) { + $rid = $loc->GetAttLazy('r:id'); + if (isset($rels->TargetLst[$rid])) { + $target = $rels->TargetLst[$rid]; + $files[] = array('file' => ('word/'.$target), 'type' => $type, 'place' => $place); + } + } + } + } + + $this->MsWord_HeaderFooter = $files; + + } + + /** + * Retrieve the header/footer sub-file. + * @param mixed $TbsCmd OPENTBS_SELECT_HEADER or OPENTBS_SELECT_FOOTER. + * @param mixed $TbsType OPENTBS_DEFAULT, OPENTBS_FIRST or OPENTBS_EVEN. + * @param int [$Offset] Since a DCX can have several sections, and each section can have its own header/footer, this options + * @return mixed The name of the file of false if no file is found. + */ + function MsWord_GetHeaderFooterFile($TbsCmd, $TbsType, $Offset = 0) { + + $this->MsWord_InitHeaderFooter(); + + $Place = 'header'; + if ($TbsCmd==OPENTBS_SELECT_FOOTER) { + $Place = 'footer'; + } + + $Type = 'default'; + if ($TbsType==OPENTBS_FIRST) { + $Type = 'first'; + } elseif ($TbsType==OPENTBS_EVEN) { + $Type = 'even'; + } + + $nb = 0; + foreach($this->MsWord_HeaderFooter as $info) { + if ( ($info['type']==$Type) && ($info['place']==$Place) ) { + if ($nb==$Offset) { + return $info['file']; + } else { + $nb++; + } + } + } + + return false; + + } + + function MsWord_DocDebug($nl, $sep, $bull) { + + $ChartLst = $this->OpenXML_ChartGetInfoFromFile($this->Ext_GetMainIdx()); + + echo $nl; + echo $nl."Charts found in the body:"; + echo $nl."-------------------------"; + foreach ($ChartLst as $i=>$c) { + $name = ($c['name']===false) ? '(not found)' : $c['name']; + $title = ($c['title']===false) ? '(not found)' : var_export($c['title'], true); + echo $bull."name: '$name', title: $title"; + if ($c['descr']!==false) echo ", description: ".$c['descr']; + } + + } + + // OpenOffice documents + + /** + * Convert a string to an attribut’s value in OpenDoc + */ + function OpenDoc_AttVal($x) { + // Replace <>&"' + return htmlspecialchars($x, ENT_QUOTES + ENT_SUBSTITUTE); + } + + function OpenDoc_CleanRsID(&$Txt) { + + // Get all style names about RSID for elements + $styles = array(); + $p = 0; + while ( ($el = clsTbsXmlLoc::FindStartTagHavingAtt($Txt, 'officeooo:rsid', $p)) !== false) { + // If the element has only this attribute then its length is 50. + if ($el->GetLen() < 60) { + if ($par = clsTbsXmlLoc::FindStartTag($Txt, 'style:style', $el->PosBeg, false)) { + if ($name = $par->GetAttLazy('style:name')) { + $styles[] = $name; + } + } + } + $p = $el->PosEnd; + } + + // Delete elements + $xe = ''; + $xe_len = strlen($xe); + foreach ($styles as $name) { + $p = 0; + $x = ''; + $x_len = strlen($x); + while ( ($p = strpos($Txt, $x, $p)) !== false) { + $pe = strpos($Txt, $xe, $p); + $src = substr($Txt, $p + $x_len, $pe - $p - $x_len); + $Txt = substr_replace($Txt, $src, $p, $pe + $xe_len - $p); + $p = $p + strlen($src); + } + } + + } + + function OpenDoc_ManifestChange($Path, $Type) { + // Set $Type=false in order to mark the the manifest entry to be deleted. + // Video and sound files are not to be registered in the manifest since the contents is not saved in the document. + + // Initialization + if ($this->OpenDocManif===false) $this->OpenDocManif = array(); + + // We try to found the type of image + if (($Type==='') && (substr($Path,0,9)==='Pictures/')) { + $ext = basename($Path); + $p = strrpos($ext, '.'); + if ($p!==false) { + $ext = strtolower(substr($ext,$p+1)); + if (isset($this->ExtInfo['pic_ext'][$ext])) $Type = 'image/'.$this->ExtInfo['pic_ext'][$ext]; + } + } + + $this->OpenDocManif[$Path] = $Type; + + } + + function OpenDoc_ManifestCommit($Debug) { + + // Retrieve the content of the manifest + $name = 'META-INF/manifest.xml'; + $idx = $this->FileGetIdx($name); + if ($idx===false) return; + + $Txt = $this->TbsStoreGet($idx, 'OpenDocumentFormat'); + if ($Txt===false) return false; + + // Perform all changes + foreach ($this->OpenDocManif as $Path => $Type) { + $x = 'manifest:full-path="'.$Path.'"'; + $p = strpos($Txt,$x); + if ($Type===false) { + // the entry should be deleted + if ($p!==false) { + $p1 = strrpos(substr($Txt,0,$p), '<'); + $p2 = strpos($Txt,'>',$p); + if (($p1!==false) && ($p2!==false)) $Txt = substr($Txt,0,$p1).substr($Txt,$p2+1); + } + } else { + // the entry should be added + if ($p===false) { + $p = strpos($Txt,''); + if ($p!==false) { + $x = ' '."\n"; + $Txt = substr_replace($Txt, $x, $p, 0); + } + } + } + } + + // Save changes (no need to save it in the park because this fct is called after merging) + $this->FileReplace($idx, $Txt); + + if ($Debug) $this->DebugLst[$name] = $Txt; + + } + + function OpenDoc_ChangeCellType(&$Txt, &$Loc, $Ope, $IsMerging, &$Value) { + // change the type of a cell in an ODS file + + $Loc->PrmLst['cellok'] = true; // avoid the field to be processed twice + + if ($Ope==='odsStr') return true; + + static $OpeLst = array( + 'tbs:num'=>'float', + 'tbs:percent'=>'percentage', + 'tbs:curr'=>'currency', + 'tbs:bool'=>'boolean', + 'tbs:date'=>'date', + 'tbs:time'=>'time', + // for compatibility + 'odsNum'=>'float', + 'odsPercent'=>'percentage', + 'odsCurr'=>'currency', + 'odsBool'=>'boolean', + 'odsDate'=>'date', + 'odsTime'=>'time', + ); + + static $TypeLst = array( + 'float' => array('attval' => 'office:value'), + 'percentage' => array('attval' => 'office:value'), + 'currency' => array('attval' => 'office:value', 'attcurr' => 'office:currency'), + 'boolean' => array('attval' => 'office:boolean-value'), + 'date' => array('attval' => 'office:date-value', 'frm' => 'yyyy-mm-ddThh:nn:ss'), + 'time' => array('attval' => 'office:time-value', 'frm' => '"PT"hh"H"nn"M"ss"S"'), + ); + + if (!isset($OpeLst[$Ope])) return false; + + $new_type = $OpeLst[$Ope]; + $new_atts = $TypeLst[$new_type]; + + $xLoc = clsTbsXmlLoc::FindStartTag($Txt, 'table:table-cell', $Loc->PosBeg, false); + if ($xLoc===false) return false; // error in the XML structure + + // Replace the current TBS field with blank chars + // This prevent from cases when the TBS field is not inside the cell (is this even possible ?) + $len = $Loc->PosEnd - $Loc->PosBeg + 1; + $Txt = substr_replace($Txt, str_repeat(' ',$len), $Loc->PosBeg, $len); + + $xLoc->switchToRelative(); + + // Update attributes + $xLoc->DeleteAtt('calcext:value-type'); // new attribute in LibreOffice 4 + $xLoc->ReplaceAtt('office:value-type', $new_type, true); + $xLoc->ReplaceAtt($new_atts['attval'], '[]', true); // [] are the new bounds of the TBS field + if (isset($new_atts['attcurr']) && isset($Loc->PrmLst['currency'])) $xLoc->ReplaceAtt('office:currency', $Loc->PrmLst['currency'], true); + + // Delete contents + $xLocP = clsTbsXmlLoc::FindElement($xLoc, 'text:p', 0); + if ($xLocP!==false) { + $xLocP->Delete(); + $xLocP->UpdateParent(); + } + + // move the TBS field + $p_fld = strpos($xLoc->Txt, '[', 0); // new position of the fields in $Txt + + $xLoc->switchToNormal(); + + $Loc->PosBeg = $xLoc->PosBeg + $p_fld; + $Loc->PosEnd = $xLoc->PosBeg + $p_fld +1; + + if ($IsMerging) { + // the field is currently being merged + if ($new_type==='boolean') { + if ($Value) { + $Value = 'true'; + } else { + $Value = 'false'; + } + } elseif (isset($new_atts['frm'])) { + $prm = array('frm'=>$new_atts['frm']); + $Value = $this->TBS->meth_Misc_Format($Value,$prm); + } + $Loc->ConvStr = false; + $Loc->ConvProtect = false; + } else { + if (isset($new_atts['frm'])) $Loc->PrmLst['frm'] = $new_atts['frm']; + } + + } + + function OpenDoc_SheetSlides_Init($sheet, $force = false) { + + if (($this->OpenDoc_SheetSlides!==false) && (!$force) ) return; + + $this->OpenDoc_SheetSlides = array(); // sheet/slide info sorted by location + + $idx = $this->Ext_GetMainIdx(); + if ($idx===false) return; + $Txt = $this->TbsStoreGet($idx, 'Sheet/Slide Info'); + if ($Txt===false) return false; + if ($this->LastReadNotStored) $this->TbsStorePut($idx, $Txt); + $this->OpenDoc_SheetSlides_FileId = $idx; + + $tag = ($sheet) ? 'table:table' : 'draw:page'; + + // scann sheet/slide list + $p = 0; + $idx = 0; + while ($loc=clsTinyButStrong::f_Xml_FindTag($Txt, $tag, true, $p, true, false, true, true) ) { + $this->OpenDoc_SheetSlides[$idx] = $loc; + $idx++; + $p = $loc->PosEnd; + } + + } + + // Actally delete hide or display Sheets and Slides in a ODS or ODP + function OpenDoc_SheetSlides_DeleteAndDisplay($sheet) { + + if ( (count($this->OtbsSheetSlidesDelete)==0) && (count($this->OtbsSheetSlidesVisible)==0) ) return; + + $this->OpenDoc_SheetSlides_Init($sheet, true); + $Txt = $this->TbsStoreGet($this->OpenDoc_SheetSlides_FileId, 'Sheet Delete and Display'); + + if ($sheet) { + // Sheet + $tag_close = ''; + $att_name = 'table:name'; + $att_style = 'table:style-name'; + $att_display = 'table:display'; + $yes_display = 'true'; + $not_display = 'false'; + $tag_property = 'style:table-properties'; + } else { + // Slide + $tag_close = ''; + $att_name = 'draw:name'; + $att_style = 'draw:style-name'; + $att_display = 'presentation:visibility'; + $yes_display = 'visible'; + $not_display = 'hidden'; + $tag_property = 'style:drawing-page-properties'; + } + $tag_close_len = strlen($tag_close); + + $styles_to_edit = array(); + // process sheet in rever order of their positions + for ($idx = count($this->OpenDoc_SheetSlides) - 1; $idx>=0; $idx--) { + $loc = $this->OpenDoc_SheetSlides[$idx]; + $id = 'i:'.($idx + 1); + $name = 'n:'.$loc->PrmLst[$att_name]; + if ( isset($this->OtbsSheetSlidesDelete[$name]) || isset($this->OtbsSheetSlidesDelete[$id]) ) { + // Delete the sheet + $p = strpos($Txt, $tag_close, $loc->PosEnd); + if ($p===false) return; // XML error + $Txt = substr_replace($Txt, '', $loc->PosBeg, $p + $tag_close_len - $loc->PosBeg); + unset($this->OtbsSheetSlidesDelete[$name]); + unset($this->OtbsSheetSlidesDelete[$id]); + unset($this->OtbsSheetSlidesVisible[$name]); + unset($this->OtbsSheetSlidesVisible[$id]); + } elseif ( isset($this->OtbsSheetSlidesVisible[$name]) || isset($this->OtbsSheetSlidesVisible[$id]) ) { + // Hide or dispay the sheet + $visible = (isset($this->OtbsSheetSlidesVisible[$name])) ? $this->OtbsSheetSlidesVisible[$name] : $this->OtbsSheetSlidesVisible[$id]; + $visible = ($visible) ? $yes_display : $not_display; + if (isset($loc->PrmLst[$att_style])) { + $style = $loc->PrmLst[$att_style]; + $new = $style.'_tbs_'.$visible; + if (!isset($styles_to_edit[$style])) $styles_to_edit[$style] = array(); + $styles_to_edit[$style][$visible] = $new; // mark the style to be edited + $pi = $loc->PrmPos[$att_style]; + $Txt = substr_replace($Txt, $pi[4].$new.$pi[4], $pi[2], $pi[3]-$pi[2]); + } + unset($this->OtbsSheetSlidesVisible[$name]); + unset($this->OtbsSheetSlidesVisible[$id]); + } + } + + // process styles to edit + if (count($styles_to_edit)>0) { + $tag_close = ''; + $tag_close_len = strlen($tag_close); + $p = 0; + while ($loc=clsTinyButStrong::f_Xml_FindTag($Txt, 'style:style', true, $p, true, false, true, false) ) { + $p = $loc->PosEnd; + if (isset($loc->PrmLst['style:name'])) { + $name = $loc->PrmLst['style:name']; + if (isset($styles_to_edit[$name])) { + // retrieve the full source of the element + $p = strpos($Txt, $tag_close, $p); + if ($p===false) return; // bug in the XML contents + $p = $p + $tag_close_len; + $src = substr($Txt, $loc->PosBeg, $p - $loc->PosBeg); + // add the attribute, if missing + if (strpos($src, ' '.$att_display.'="')===false) $src = str_replace('<'.$tag_property.' ', '<'.$tag_property.' '.$att_display.'="'.$yes_display.'" ', $src); + // add new styles + foreach ($styles_to_edit[$name] as $visible => $newName) { + $not = ($visible===$not_display) ? $yes_display : $not_display; + $src2 = str_replace(' style:name="'.$name.'"', ' style:name="'.$newName.'"', $src); + $src2 = str_replace(' '.$att_display.'="'.$not.'"', ' '.$att_display.'="'.$visible.'"', $src2); + $Txt = substr_replace($Txt, $src2, $loc->PosBeg, 0); + $p = $p + strlen($src2); + } + } + } + } + + } + + // store the result + $this->TbsStorePut($this->OpenDoc_SheetSlides_FileId, $Txt); + + $this->TbsSheetCheck(); + + } + + function OpenDoc_SheetSlides_Debug($sheet, $nl, $sep, $bull) { + + $this->OpenDoc_SheetSlides_Init($sheet); + + $text = ($sheet) ? "Sheets in the Workbook" : "Slides in the Presentation"; + $att = ($sheet) ? 'table:name' : 'draw:name'; + + echo $nl; + echo $nl.$text.":"; + echo $nl."-----------------------"; + foreach ($this->OpenDoc_SheetSlides as $idx => $loc) { + $name = str_replace(array('&','"','<','>'), array('&','"','<','>'), $loc->PrmLst[$att]); + echo $bull."id: ".($idx+1).", name: [".$name."]"; + } + + } + + function OpenDoc_StylesInit() { + + if ($this->OpenDoc_Styles!==false) return; + + $this->OpenDoc_Styles = array(); // sheet info sorted by location + + $Styles = array(); + + // Read styles in 'styles.xml' + $idx = $this->FileGetIdx('styles.xml'); + if ($idx!==false) { + $Txt = $this->TbsStoreGet($idx, 'Style Init styles.xml'); + if ($Txt==!false) $this->OpenDoc_StylesFeed($Styles, $Txt); + } + + // Read styles in 'content.xml' + $idx = $this->FileGetIdx('content.xml'); + if ($idx!==false){ + $Txt = $this->TbsStoreGet($idx, 'Style Init content.xml'); + if ($Txt!==false) $this->OpenDoc_StylesFeed($Styles, $Txt); + } + + // define childs + foreach($Styles as $n => $s) { + if ( ($s->parentName!==false) && isset($Styles[$s->parentName]) ) $Styles[$s->parentName]->childs[$s->name] = &$s; + } + + // propagate page-break property to alla childs + $this->OpenDoc_StylesPropagate($Styles); + + $this->OpenDoc_Styles = $Styles; + + } + + function OpenDoc_RangeNamesInit() { + + if ($this->OtbsSheetRangeNames !== false) return; + + $this->OtbsSheetRangeNames = array(); + + // Get the content.xml contents + $idx = $this->FileGetIdx('content.xml'); + if ($idx===false) return; + $Txt = $this->TbsStoreGet($idx, 'RangeNamesInit'); // use the store, so the file will be available for editing if needed + if ($Txt===false) return false; + //$this->TbsStorePut($idx, $Txt); + + // Sheet ranges + $p = 0; + while ( $el = clsTbsXmlLoc::FindElement($Txt, 'table:named-range', $p, true) ) { + $name = $el->GetAttLazy('table:name'); + $ref = $el->GetAttLazy('table:cell-range-address'); + if ($ref === false) { + $ref = $el->GetAttLazy('table:base-cell-address'); // can be a single cell reference + } + $this->OtbsSheetRangeNames[$name] = $this->Sheet_GetRangeInfo($ref); + $p = $el->PosEnd; + } + + // Database ranges + $p = 0; + while ( $el = clsTbsXmlLoc::FindElement($Txt, 'table:database-range', $p, true) ) { + $name = $el->GetAttLazy('table:name'); + // A Database Range can have the same name as a Sheet Range. Priority to the Sheet Range. + if (!isset($this->OtbsSheetRangeNames[$name])) { + $ref = $el->GetAttLazy('table:target-range-address'); + $this->OtbsSheetRangeNames[$name] = $this->Sheet_GetRangeInfo($ref); + } + $p = $el->PosEnd; + } + + } + + /** + * Get the locator of the sheet element. + * @param array $Range The range information. + * @return object A clsTbsXmlLoc locator of false if error. + */ + function OpenDoc_GetSheetLoc($Range) { + + $idx = $this->FileGetIdx('content.xml'); + $Txt = $this->TbsStoreGet($idx, 'OpenDoc_GetSheetLoc'); + + // Find the sheet + if ($Range['sheet']) { + // Find the named sheet + $pos = 0; + $cont = true; + while ($cont) { + if ($SheetLoc = clsTbsXmlLoc::FindStartTag($Txt, 'table:table', $pos, true)) { + if ($SheetLoc->GetAttLazy('table:name') === $Range['sheet']) { + $SheetLoc->FindEndTag(); + + $cont = false; + } else { + $pos = $SheetLoc->PosEnd; + } + } else { + $cont = false; + } + } + } else { + // Get the first sheet + $SheetLoc = clsTbsXmlLoc::FindElement($Txt, 'table:table', 0, true); + } + + + return $SheetLoc; + + } + + function OpenDoc_GetCellValue($Loc) { + + $x = null; + + if ( $Loc->Exists && ($Loc->GetInnerStart() !== false) ) { + $type = $Loc->GetAttLazy('office:value-type'); + if ($type === 'string') { + // Errors are in this case, but with attribute { calcext:value-type="error" } + // A simple text is ebedded in a . Line breaks are made with several . Formatinf are made with + $x = $Loc->GetInnerSrc(); + $x = str_replace('', '', $x); + $x = str_replace('', "\n", $x); // replace new paragraph with line breaks + $x = strip_tags($x); // take of formating + } elseif ($type === 'time') { + $z = $Loc->GetAttLazy('office:time-value'); + if ($z !== false) { + // 'PT12H23M00S', 'P' means period + $x = $z; + } + } elseif ($type === 'date') { + $z = $Loc->GetAttLazy('office:date-value'); + if ($z !== false) { + // Date or DateTime quite ISO 8601 + // '2019-04-06', '2018-12-11T11:45:00' + $x = $z; + } + } elseif ($type === 'boolean') { + $z = $Loc->GetAttLazy('office:boolean-value'); + if ($z === 'true') { + $x = true; + } elseif ($x === 'false') { + $z = false; + } + } else { + // float, percentage, currency + $z = $Loc->GetAttLazy('office:value'); + if ($z !== false) { + $x = floatval($z); + } + } + } + + return $x; + + } + + // Feed $Styles with styles found in $Txt + function OpenDoc_StylesFeed(&$Styles, $Txt) { + $p = 0; + while ($loc = clsTbsXmlLoc::FindElement($Txt, 'style:style', $p)) { + unset($o); + $o = (object) null; + $o->name = $loc->GetAttLazy('style:name'); + $o->parentName = $loc->GetAttLazy('style:parent-style-name'); + $o->childs = array(); + $o->pbreak = false; + $o->ctrl = false; + $src = $loc->GetSrc(); + if (strpos($src, ' fo:break-before="page"')!==false) $o->pbreak = 'before'; + if (strpos($src, ' fo:break-after="page"')!==false) $o->pbreak = 'after'; + if ($o->name!==false) $Styles[$o->name] = $o; + $p = $loc->PosEnd; + } + } + + function OpenDoc_StylesPropagate(&$Styles) { + foreach ($Styles as $i => $s) { + if (!$s->ctrl) { + $s->ctrl = true; // avoid circular reference + if ($s->pbreak!==false) { + foreach ($s->childs as $j => $c) { + if ($c->pbreak!==false) $c->pbreak = $s->pbreak; + $this->OpenDoc_StylesPropagate($c); + } + } + $s->childs = false; + } + } + } + + // TBS Block Alias for pages + function OpenDoc_GetPage($Tag, $Txt, $Pos, $Forward, $LevelStop) { + + $this->OpenDoc_StylesInit(); + + $p = $Pos; + + while ( ($loc = clsTbsXmlLoc::FindStartTagHavingAtt($Txt, 'text:style-name', $p, $Forward))!==false) { + + $style = $loc->GetAttLazy('text:style-name'); + + if ( ($style!==false) && isset($this->OpenDoc_Styles[$style]) ) { + $pbreak = $this->OpenDoc_Styles[$style]->pbreak; + if ($pbreak!==false) { + if ($Forward) { + // Forward + if ($pbreak==='before') { + return $loc->PosBeg -1; // note that the page-break is not in the block + } else { + $loc->FindEndTag(); + return $loc->PosEnd; + } + } else { + // Backward + if ($pbreak==='before') { + return $loc->PosBeg; + } else { + $loc->FindEndTag(); + return $loc->PosEnd+1; // note that the page-break is not in the block + } + } + } + } + + $p = ($Forward) ? $loc->PosEnd : $loc->PosBeg; + + } + + // If we are here, then no tag is found, we return the boud of the main element + if ($Forward) { + $p = strpos($Txt, 'PosEnd + 1; + } + + } + + // TBS Block Alias for draws + function OpenDoc_GetDraw($Tag, $Txt, $Pos, $Forward, $LevelStop) { + return $this->XML_BlockAlias_Prefix('draw:', $Txt, $Pos, $Forward, $LevelStop); + } + + /** + * Find a chart in the template by its reference. + * Return an array of technical information about the sub-file. + */ + function OpenDoc_ChartFind($ChartRef, &$Txt, $ErrTitle) { + + if ($this->OpenDocCharts===false) $this->OpenDoc_ChartInit(); + + // Find the chart + if (is_numeric($ChartRef)) { + $ChartCaption = 'number ' . $ChartRef; + $idx = intval($ChartRef) -1; + if (!isset($this->OpenDocCharts[$idx])) return $this->RaiseError("($ErrTitle) : unable to find the chart $ChartCaption."); + } else { + $ChartCaption = 'corresponding to "' . $ChartRef . '"'; + $idx = false; + $x = htmlspecialchars($ChartRef, ENT_NOQUOTES); // ENT_NOQUOTES because target is an element's content + $fld = $this->OpenDoc_AttVal('[' . $ChartRef . ']'); // tag to search in the Alt Text + foreach($this->OpenDocCharts as $i=>$c) { + // Title is captioned "Alternative (text only)" in ODT. So we search for $fld in both title and description in order to be consistent with Ms Office wich has Description captioned Alt Text. + if ( ($c['title'] == $x) || (strpos($c['title'], $fld ) !== false) || (strpos($c['descr'], $fld ) !== false) ) { + $idx = $i; + } + } + if ($idx===false) return $this->RaiseError("($ErrTitle) : unable to find the chart $ChartCaption."); + } + $this->_ChartCaption = $ChartCaption; // for error messages + + // Retrieve chart information + $chart = &$this->OpenDocCharts[$idx]; + if ($chart['to_clear']) $this->OpenDoc_ChartClear($chart); + + // Retrieve the XML of the data + $file_name = $chart['href'] . '/content.xml'; + $file_idx = $this->FileGetIdx($file_name); + if ($file_idx===false) return $this->RaiseError("($ErrTitle) : unable to find the data in the chart $ChartCaption."); + $chart['file_name'] = $file_name; + $chart['file_idx'] = $file_idx; + + $Txt = $this->TbsStoreGet($file_idx, 'OpenDoc_ChartChangeSeries'); + + // Found all chart series + if (!isset($chart['series'])) { + $ok = $this->OpenDoc_ChartFindSeries($chart, $Txt); + if (!$ok) return false; + } + + return $chart; + + } + + function OpenDoc_ChartChangeSeries($ChartRef, $SeriesNameOrNum, $NewValues, $NewLegend=false) { + + $Txt = false; + $chart = $this->OpenDoc_ChartFind($ChartRef, $Txt, 'ChartChangeSeries'); + if ($chart === false) return; + + $series = &$chart['series']; + + // Found the asked series + $s_info = false; + if (is_numeric($SeriesNameOrNum)) { + $s_caption = 'number '.$SeriesNameOrNum; + $idx = $SeriesNameOrNum -1; + if (isset($series[$idx])) $s_info = &$series[$idx]; + } else { + $s_caption = '"'.$SeriesNameOrNum.'"'; + foreach($series as $idx => $s) { + if ( ($s_info===false) && ($s['name']==$SeriesNameOrNum) ) $s_info = &$series[$idx]; + } + } + if ($s_info===false) return $this->RaiseError("(ChartChangeSeries) : unable to find the series $s_caption in the chart ".$this->_ChartCaption."."); + + if ($NewLegend!==false) $this->OpenDoc_ChartRenameSeries($Txt, $s_info, $NewLegend); + + if ($s_info['local']) { + $this->OpenDoc_ChartUnlinkSeries($Txt, $chart, $s_info); + } + + // simplified variables + $col_cat = $chart['series_cat_col']; // column Category (always 0) + $col_nb = $chart['tbl_col_nb']; // number of columns + $s_col_deb = min($s_info['used_cols']); // first data column of the series + $s_col_nb = count($s_info['used_cols']); + $s_col_end = $s_col_deb + $s_col_nb - 1; // last column of the series + $s_use_cat = (count($s_info['used_cols'])==1); // true if the series uses the column Category + + // Force syntax of data + if (!is_array($NewValues)) { + $data = array(); + if ($NewValues===false) $this->OpenDoc_ChartDelSeries($Txt, $s_info); + } elseif ( $s_use_cat && isset($NewValues[0]) && isset($NewValues[1]) && is_array($NewValues[0]) && is_array($NewValues[1]) ) { + // syntax 2: $NewValues = array( array('cat1','cat2',...), array(val1,val2,...) ); + $k = $NewValues[0]; + $v = $NewValues[1]; + $data = array(); + foreach($k as $i=>$x) $data[$x] = isset($v[$i]) ? $v[$i] : false; + unset($k, $v); + } else { + // syntax 1: $NewValues = array( 'cat1'=>val1, 'cat2'=>val2, ... ); + $data = $NewValues; + } + unset($NewValues); + + // Scann all rows (=categories) for changing cells + $elData = clsTbsXmlLoc::FindElement($Txt, 'table:table-rows', 0); + $p_row = 0; + while (($elRow=clsTbsXmlLoc::FindElement($elData, 'table:table-row', $p_row))!==false) { + $p_cell = 0; + $category = false; + $data_r = false; + for ($i = 0 ; $i <= $s_col_end ; $i++) { + if ($elCell = clsTbsXmlLoc::FindElement($elRow, 'table:table-cell', $p_cell)) { + if ($i == $col_cat) { + // Category + if ($el = clsTbsXmlLoc::FindElement($elCell, 'text:p', 0)) { + $category = $el->GetInnerSrc(); + } + } elseif ($i >= $s_col_deb) { + // Change the value + $x = 'NaN'; // default value + if ($s_use_cat) { + if ( ($category!==false) && isset($data[$category]) ) { + $x = $data[$category]; + unset($data[$category]); // delete the category in order to keep only unused + } + } else { + $val_idx = $i - $s_col_deb; + if ($data_r===false) $data_r = array_shift($data); // (may return null) delete the row in order to keep only unused + if ( (!is_null($data_r)) && isset($data_r[$val_idx])) $x = $data_r[$val_idx]; + } + if ( ($x===false) || is_null($x) ) $x = 'NaN'; + $elCell->ReplaceAtt('office:value', $x); + // Delete the cached legend + if ($el = clsTbsXmlLoc::FindElement($elCell, 'text:p', 0)) { + $el->ReplaceSrc(''); + $el->UpdateParent(); // update $elCell source + } + // Delete reference to worksheet + if ($el = clsTbsXmlLoc::FindElement($elCell, 'draw:g', 0)) { + $el->ReplaceSrc(''); + $el->UpdateParent(); // update $elCell source + } + $elCell->UpdateParent(); // update $elRow source + } + $p_cell = $elCell->PosEnd; + } else { + $i = $s_col_end + 1; // ends the loops + } + } + $elRow->UpdateParent(); // update $elData source + $p_row = $elRow->PosEnd; + } + + // Add unused data + // TODO : we should update the category range in but LibreOffice seems to not care about it. + $x = ''; + $x_nan = ''; + foreach ($data as $cat=>$val) { + $x .= ''; + if ($s_use_cat) $val = array($val); + for ($i = 0 ; $i <= $col_nb -1 ; $i++) { + if ( ($s_col_deb <= $i) && ($i <= $s_col_end) ) { + $val_idx = $i - $s_col_deb; + if (isset($val[$val_idx])) { + $x .= ''; + } else { + $x .= $x_nan; + } + } else { + if ($s_use_cat && ($i == $col_cat) ) { + // ENT_NOQUOTES because target is an element's content + $x .= ''.htmlspecialchars($cat, ENT_NOQUOTES).''; + } else { + $x .= $x_nan; + } + } + } + $x .= ''; + } + $p = strpos($Txt, '', $elData->PosBeg); + if ($x!=='') $Txt = substr_replace($Txt, $x, $p, 0); + + // Save the result + $this->TbsStorePut($chart['file_idx'], $Txt); + + } + + /** + * Look for all chart in the document, and store information. + */ + function OpenDoc_ChartInit() { + + $this->OpenDocCharts = array(); + + $idx = $this->Ext_GetMainIdx(); + $Txt = $this->TbsStoreGet($idx, 'OpenDoc_ChartInit'); + + $p = 0; + while($drEl = clsTbsXmlLoc::FindElement($Txt, 'draw:frame', $p)) { + + $src = $drEl->GetInnerSrc(); + $objEl = clsTbsXmlLoc::FindStartTag($src, 'draw:object', 0); + + if ($objEl) { // Picture have without + $href = $objEl->GetAttLazy('xlink:href'); // example "./Object 1" + if ($href) { + + $el = clsTbsXmlLoc::FindElement($src, 'draw:image', 0); + $img_href = ($el) ? $el->GetAttLazy('xlink:href') : false; // "./ObjectReplacements/Object 1" + $img_src = ($el) ? $el->GetSrc('xlink:href') : false; + + $el = clsTbsXmlLoc::FindElement($src, 'svg:title', 0); // Caption is "Altenative (text only)" in ODT + $title = ($el) ? $el->GetInnerSrc() : ''; + + $el = clsTbsXmlLoc::FindElement($src, 'svg:desc', 0); // + $descr = ($el) ? $el->GetInnerSrc() : ''; + + if (substr($href,0,2)=='./') $href = substr($href, 2); + if ( is_string($img_href) && (substr($img_href,0,2)=='./') ) $img_href = substr($img_href, 2); + $this->OpenDocCharts[] = array('href'=>$href, 'title'=>$title, 'descr' => $descr, 'img_href'=>$img_href, 'img_src'=>$img_src, 'to_clear'=> ($img_href!==false) ); + + } + } + $p = $drEl->PosEnd; + } + + + } + + /** + * Clear some of the chart informations : + * - Delete the picture of the chart which is used as a snapshot. + */ + function OpenDoc_ChartClear(&$chart) { + + $chart['to_clear'] = false; + + // Delete the file in the archive + $idx = $this->FileGetIdx($chart['img_href']); + // One test with ODS had a referenced picture that did not exist in the archive. + if ($idx !== false) { + $this->FileReplace($idx, false); + } + + // Delete the element in the main file + $main = $this->Ext_GetMainIdx(); + $Txt = $this->TbsStoreGet($main, 'OpenDoc_ChartClear'); + $Txt = str_replace($chart['img_src'], '', $Txt); + $this->TbsStorePut($main, $Txt); + + // Delete the element in the Manifest file + $manifest = $this->FileGetIdx('META-INF/manifest.xml'); + if ($manifest!==false) { + $Txt = $this->TbsStoreGet($manifest, 'OpenDoc_ChartClear'); + $el = clsTbsXmlLoc::FindStartTagHavingAtt($Txt, 'manifest:full-path="'.$chart['img_href']."'", 0); + if ($el) { + $el->ReplaceSrc(''); + $this->TbsStorePut($manifest, $Txt); + } + } + + } + + /** + * Find and save informations abouts all series in the chart. + */ + function OpenDoc_ChartFindSeries(&$chart, $Txt) { + + // Find series declarations + $p = 0; + $s_idx = 0; + $series = array(); + while ($elSeries = clsTbsXmlLoc::FindElement($Txt, 'chart:series', $p)) { + + // List of column's nums for other values + $used_refs = array(); + $used_refs[] = $elSeries->GetAttLazy('chart:values-cell-range-address'); + + $src = $elSeries->GetInnerSrc(); + $p2 = 0; + // is used by type of charts: scatter, bubble, surface inorder to save X and Y values. + // It may have 0, 1 or 2 entity in each series. 1 entity means X is the num order of the row, and Y is the column given by the domain. + while ($elDom = clsTbsXmlLoc::FindStartTag($src, 'chart:domain', $p2)) { + $att = $elDom->GetAttLazy('table:cell-range-address'); + if ($att) { + $used_refs[] = $att; + } + $p2 = $elDom->PosEnd; + } + + // Attribute to re-find the series + $label_ref = $elSeries->GetAttLazy('chart:label-cell-address'); + + // Add the series + $series[$s_idx] = array( + 'name' => false, // name of the series + 'name_col' => false, // colmun index for the name of the series (always on first row) + 'val_col' => false, // column index for the values + 'used_cols' => false, // list of column indexes in the table that are used by the series + 'used_refs' => $used_refs, // list of ref usde by the series, (0] is always for values + 'label_ref' => $label_ref, + 'local' => true, // indicate if data is stored in the local table (flase means linked to cells) + ); + $p = $elSeries->PosEnd; + $s_idx++; + } + + // Column of categories + $series_cat_ref = false; + $elCat = clsTbsXmlLoc::FindStartTag($Txt, 'chart:categories', 0); + if ($elCat!==false) { + $series_cat_ref = $elCat->GetAttLazy('table:cell-range-address'); + } + + $elTbl = clsTbsXmlLoc::FindStartTag($Txt, 'table:table', 0); + if ($elTbl===false) return $this->RaiseError("(ChartFindSeries) : unable to find the local table in the chart ".$this->_ChartCaption."."); + $tbl_name = $elTbl->GetAttLazy('table:name'); + + // Info in the table + $tbl_columns = array(); + $tbl_cat_ref = ''; + $p = 0; + + // Browse headers columns + $elRow = clsTbsXmlLoc::FindElement($Txt, 'table:table-header-rows', $elTbl->PosBeg); + if ($elRow === false) return $this->RaiseError("(ChartFindSeries) : unable to find the header row in the chart ".$this->_ChartCaption."."); + + $col_idx = -1; + while (($elCell = clsTbsXmlLoc::FindElement($elRow, 'table:table-cell', $p))!==false) { + $col_idx++; + // Text in the cell + $el = clsTbsXmlLoc::FindElement($elCell, 'text:p', 0); + $name = ($el===false) ? '' : $el->GetInnerSrc(); + // Ref in the cell + $el = clsTbsXmlLoc::FindElement($elCell, 'svg:desc', 0); + $label_ref = ($el===false) ? false : $el->GetInnerSrc(); + // Series info + $tbl_columns[$col_idx] = array('name' => $name, 'label_ref' => $label_ref, 'val_ref' => false); + $p = $elCell->PosEnd; + } + + // If the chart is link to a worksheet then the first row contains refrences to the cells + // Browse first row + $elRow = clsTbsXmlLoc::FindElement($Txt, 'table:table-row', $elRow->PosEnd); + if ($elRow === false) return $this->RaiseError("(ChartFindSeries) : unable to find the first data row in the chart ".$this->_ChartCaption."."); + + $tbl_cat_ref = false; + $col_idx = -1; + $p = 0; + while (($elCell = clsTbsXmlLoc::FindElement($elRow, 'table:table-cell', $p))!==false) { + $col_idx++; + // Ref in the cell. + $el = clsTbsXmlLoc::FindElement($elCell, 'svg:desc', 0); + if ($el !== false) { + if ($col_idx == 0) { + $tbl_cat_ref = $el->GetInnerSrc(); + } else { + // + $tbl_columns[$col_idx]['val_ref'] = $el->GetInnerSrc(); + } + } + $p = $elCell->PosEnd; + } + + // Experimentally first colmun is for categories, and then for each series it is somain cols first and then val col. + $def_col_idx = 0; + + // Match info between series info and the table + foreach ($series as $ser_idx => $info) { + + $def_col_idx = $def_col_idx + count($info['used_refs']); + + // Name of the series + $col_idx = $this->OpenDoc_FirstColIdx($info['label_ref'], $tbl_name, $def_col_idx); + $info['name_col'] = $col_idx; + if (isset($tbl_columns[$col_idx])) { + $info['name'] = $tbl_columns[$col_idx]['name']; + } + + // The colmun for values is supposed to be the first column used for values in the series + $info['used_cols'] = array(); + foreach ($info['used_refs'] as $idx => $ref) { + $col_idx = $this->OpenDoc_FirstColIdx($ref, $tbl_name, ($def_col_idx - $idx)); + if ($col_idx === false) { + $info['local'] = false; + $col_idx = ($def_col_idx - $idx); + } + if ($idx === 0) { + $info['val_col'] = $col_idx; + } + $info['used_cols'][] = $col_idx; + } + + $series[$ser_idx] = $info; + } + + // Save info + $chart['series'] = $series; + $chart['series_cat_ref'] = $series_cat_ref; + $chart['series_cat_col'] = $this->OpenDoc_FirstColIdx($series_cat_ref, $tbl_name, 0); + $chart['tbl_name'] = $tbl_name; + $chart['tbl_col_nb'] = count($tbl_columns); + $chart['tbl_cat_ref'] = $tbl_cat_ref; + $chart['tbl_columns'] = $tbl_columns; + + return true; + + } + + /** + * Return the column number of the first cell in a range. + * + * @param string $RangeRef The reference of a range. Like "local-table.$B$2:.$B$5" + * @param string $LocTblName The local table name. The function will return false if the range is prefixed with the wrong table name. + * @param integer $def_col_idx The index return if the range is not referenced to the local table name. + * + * @return integer + */ + function OpenDoc_FirstColIdx($RangeRef, $LocTblName, $def_col_idx) { + + $p = strpos($RangeRef, '.'); + if ($p !== false) { + if (substr($RangeRef, 0 , $p) !== $LocTblName) { + // It is not the expected table + return $def_col_idx; + } + $RangeRef = substr($RangeRef, $p); // delete the table name wich is in prefix + } + $RangeRef = str_replace( array('.','$'), '', $RangeRef); + $RangeRef = explode(':', $RangeRef); + + $col_num = $this->Sheet_ColNum($RangeRef[0]); + if ($col_num === false) return $this->RaiseError('(FirstColIdx) Reference of cell \'' . $RangeRef[0] . '\' cannot be recognized.'); + + return $col_num - 1; + + } + + function OpenDoc_ChartDelSeries(&$Txt, &$series) { + + // TODO : we should update the category range in but LibreOffice seems to not care about it. + // Note: only the declaration of the series is deleted, not the data. + $att = 'chart:label-cell-address="'.$series['label_ref'].'"'; + $elSeries = clsTbsXmlLoc::FindElementHavingAtt($Txt, $att, 0); + + if ($elSeries!==false) $elSeries->ReplaceSrc(''); + + } + + /** + * Delete one, sveral or all categories in the chart. + * @param string $ChartRef The chart reference. + * @param string|array $del_categories An array of categories to delete, on the name of a category, all the keywork '*' that means all categories. + * @param boolean $no_err Indicate if an error is return when a searched category is not found. + * @return boolean Return true if all the searched categories are deleted. + */ + function OpenDoc_ChartDelCategories($ChartRef, $del_categories, $no_err) { + + $Txt = false; + $chart = $this->OpenDoc_ChartFind($ChartRef, $Txt, 'ChartChangeSeries'); + if ($chart === false) return; + + // Colmun that hold the category name + $col_idx = $chart['series_cat_col']; + + // Prepare info for the search + $del_all = false; + if (is_string($del_categories)) { + if ($del_categories == '*') { + $del_all = true; + $del_categories = array(); + } else { + $del_categories = array($del_categories); + } + } + $remain_cat = array_flip($del_categories); + + // Scann all rows for changing cells + $elData = clsTbsXmlLoc::FindElement($Txt, 'table:table-rows', 0); + $p_row = 0; + $del_nb = 0; + while ( ($elRow=clsTbsXmlLoc::FindElement($elData, 'table:table-row', $p_row)) !== false ) { + $p_cell = 0; + for ($i = 0; $i <= $col_idx; $i++) { + if ($elCell = clsTbsXmlLoc::FindElement($elRow, 'table:table-cell', $p_cell)) { + if ($i == $col_idx) { + // Category + if ($elP = clsTbsXmlLoc::FindElement($elCell, 'text:p', 0)) { + $category = $elP->GetInnerSrc(); + if ($del_all || isset($remain_cat[$category])) { + $elRow->Delete(); + $elRow->UpdateParent(true); + $del_nb++; + unset($remain_cat[$category]); + // optimisation + if ( (!$del_all) && (count($remain_cat) == 0) ) { + return true; + } + } + } + } + $p_cell = $elCell->PosEnd; + } else { + $i = $col_idx + 1; // ends the loops + } + } + $p_row = $elRow->PosEnd; + } + + // Save the file if modified + if ($del_nb > 0) { + $this->TbsStorePut($chart['file_idx'], $Txt); + } + + // Result of the function + if ( $del_all || (count($remain_cat) == 0) ) { + // All searched categories are deleted + return true; + } else { + if ($no_err) { + return false; + } else { + return $this->RaiseError("(ChartDelCategory) : unable to find categories '" . implode(', ', array_keys($remain_cat)) . "' in the chart ".$this->_ChartCaption."."); + } + } + + } + + function OpenDoc_ChartRenameSeries(&$Txt, &$series, $NewName) { + + $NewName = htmlspecialchars($NewName, ENT_NOQUOTES); // ENT_NOQUOTES because target is an element's content + $col_idx = $series['name_col']; + + $el = clsTbsXmlLoc::FindStartTag($Txt, 'table:table-header-rows', 0); + $el = clsTbsXmlLoc::FindStartTag($Txt, 'table:table-row', $el->PosEnd); + for ($i = 0; $i < $col_idx; $i++) { + $el = clsTbsXmlLoc::FindStartTag($Txt, 'table:table-cell', $el->PosEnd); + } + $elCell = clsTbsXmlLoc::FindElement($Txt, 'table:table-cell', $el->PosEnd); + + $elP = clsTbsXmlLoc::FindElement($elCell, 'text:p', 0); + if ($elP===false) { + $elCell->ReplaceInnerSrc($elCell->GetInnerSrc().''.$NewName.''); + } else { + if($elP->SelfClosing) { + $elP->ReplaceSrc(''.$NewName.''); + } else { + $elP->ReplaceInnerSrc($NewName); + } + $elP->UpdateParent(); + } + + } + + /** + * Returne the local reference for a colmun. + */ + function OpenDoc_ChartLocalColRef($chart, $col, $row1, $row2 = false) { + $ref = $chart['tbl_name'] . '.' . $this->Sheet_CellRef($col, $row1, '$'); + if ($row2 !== false) { + $ref .= ':' . $this->Sheet_CellRef($col, $row2, '$'); + } + return $ref; + } + + /** + * Unlink a series, so that is becomes attached to the local table. + * Example : "local-table.$B$2:.$B$5" + */ + function OpenDoc_ChartUnlinkSeries(&$Txt, &$chart, &$series) { + + $att = 'chart:label-cell-address="' . $series['label_ref'] .'"'; + $elSeries = clsTbsXmlLoc::FindElementHavingAtt($Txt, $att, 0); + + if ($elSeries) { + + // Replace the label adress + $col_num = $series['name_col'] + 1; + $ref = $this->OpenDoc_ChartLocalColRef($chart, $col_num, 1); + $elSeries->ReplaceAtt('chart:label-cell-address', $ref); + + // Replace the value adress + $col_num = $series['val_col'] + 1; + $ref = $this->OpenDoc_ChartLocalColRef($chart, $col_num, 2, 2); + $elSeries->ReplaceAtt('chart:values-cell-range-address', $ref); + + // Replace other adresses + $p = 0; + foreach ($series['used_cols'] as $idx => $col_idx) { + if ($col_idx != $series['val_col']) { + $el = clsTbsXmlLoc::FindStartTag($elSeries, 'chart:domain', $p); + if ($el) { + $col_num = $col_idx + 1; + $ref = $this->OpenDoc_ChartLocalColRef($chart, $col_num, 2, 2); + $el->ReplaceAtt('table:cell-range-address', $ref); + $el->UpdateParent(); + $p = $el->PosEnd; + } + } + } + + } + + $series['local'] = false; + + } + + /** + * Return information and data about all series in the chart. + */ + function OpenDoc_ChartReadSeries($ChartRef, $Complete) { + + $Txt = false; + $chart = $this->OpenDoc_ChartFind($ChartRef, $Txt, 'ChartReadSeries'); + if ($chart === false) return; + + // Read the data table + $table = array(); + $rows = clsTbsXmlLoc::FindElement($Txt, 'table:table-rows', 0); + $pr = 0; + while ($r = clsTbsXmlLoc::FindElement($rows, 'table:table-row', $pr)) { + $pr = $r->PosEnd; + $pc = 0; + $row = array(); + while ($c = clsTbsXmlLoc::FindElement($r, 'table:table-cell', $pc)) { + $pc = $c->PosEnd; + $val = $c->getAttLazy('office:value'); + if ($val == 'NaN') { // Not a Number, happens when the cell is empty + $val = false; + $txt = ''; + } else { + if ($x = clsTbsXmlLoc::FindElement($c, 'text:p', 0)) { + $txt = $x->GetInnerSrc(); + } else { + $txt = false; + }; + } + $row[] = array('val' => $val, 'txt' => $txt); + } + $table[] = $row; + } + + // Format series information + $series = array(); + $cat_idx = $chart['series_cat_col']; + foreach ($chart['series'] as $idx => $info) { + $cat = array(); + $val = array(); + $col_idx = $info['val_col']; + foreach ($table as $row) { + $val[] = $row[$col_idx]['val']; + $cat[] = $row[$cat_idx]['txt']; + } + $series[] = array( + 'name' => $info['name'], + 'cat' => $cat, + 'val' => $val, + ); + } + + if ($Complete) { + // Complete information about the chart + $main_idx = $this->Ext_GetMainIdx(); + return array( + 'file_idx' => $chart['file_idx'], + 'file_name' => $chart['file_name'], + 'parent_idx' => $main_idx, + 'parent_name' => $this->TbsGetFileName($main_idx), + 'series' => $series, + ); + } else { + // Simple information about data + $simple = array(); + foreach ($series as $s) { + $name = $s['name']; + $simple[$name] = array($s['cat'], $s['val']); + } + return $simple; + } + + } + + function OpenDoc_ChartDebug($nl, $sep, $bull) { + + if ($this->OpenDocCharts===false) $this->OpenDoc_ChartInit(); + + $ChartLst = $this->OpenDocCharts; + + echo $nl; + echo $nl."Charts found in the contents: (use command OPENTBS_CHART_INFO to get series's names and data)"; + echo $nl."-----------------------------"; + foreach ($ChartLst as $i=>$c) { + $title = ($c['title']===false) ? '(not found)' : var_export($c['title'], true); + echo $bull."title: $title"; + } + if (count($ChartLst)==0) echo $bull."(none)"; + + } + + /** + * Delete useless reapeated rows, cell and columns. + * This operation fixes the problem of ODS files built with LibreOffice >= 4 and merged with OpenTBS and opened with Ms Excel. + * The virtual number of row can exceed the maximum supported, then Excel raises an error when opening the file. + * LibreOffice does not. + */ + function OpenDoc_DeleteUselessRepeatedElements(&$Txt) { + + $el_tbl = 'table:table'; + $el_col = 'table:table-column'; // Column definition + $el_row = 'table:table-row'; + $el_cell = 'table:table-cell'; + $att_rep_col = 'table:number-columns-repeated'; + $att_rep_row = 'table:number-rows-repeated'; + + $loop = array($att_rep_col, $att_rep_row); + + // Loop for deleting useless repeated columns + foreach ($loop as $att_rep) { + + $p = 0; + while ( $xml = clsTbsXmlLoc::FindElementHavingAtt($Txt, $att_rep, $p) ) { + + $xml->FindName(); + $p = $xml->PosEnd; + + // Next tag (opening or closing) + $next = clsTbsXmlLoc::FindStartTagByPrefix($Txt, '', $p); + $next_name = $next->Name; + if ($next_name == '') { + $next_name = $next->GetSrc(); + $next_name = substr($next_name, 1, strlen($next_name) -2); + }; + + $z_src = $next->GetSrc(); + + //echo " * name=" . $xml->Name . ", suiv_name=$next_name, suiv_src=$z_src\n"; + + $delete = false; + + if ( ($xml->Name == $el_col) && ($xml->SelfClosing) ) { + if ( ($next_name == $el_row) || ($next_name == '/' . $el_tbl) ) { + // It's the last column definition of the sheet, and it is self-closing and repeated + $delete = true; + } + } elseif ( ($xml->Name == $el_cell) && ($xml->SelfClosing) ) { + if ( $next_name == '/' . $el_row ) { + // It's the last cell of a row, and it is self-closing and repeated + $delete = true; + } + } elseif ($xml->Name == $el_row) { + if ( $next_name == '/' . $el_tbl ) { + $inner_src = '' . $xml->GetInnerSrc(); + if (strpos($inner_src, '<') === false) { + // It's the last row of a sheet, and it is empty and repeated + $delete = true; + } + } + } + + if ($delete) { + //echo " * DELETE " . $xml->Name . " : " . $xml->GetSrc() . "\n"; + $p = $xml->PosBeg; + $xml->Delete(); + } + + } + + } + + } + + /** + * Apply a, OpenTBS trick in order to manage covered cells as if there are normal cells. + * + * @param string|object $src A string or an clsTbsXmlLoc object + * @param boolean $do True to apply, false to unapply. + */ + function OpenDoc_CoveredCells_Replace(&$src, $do) { + + $is_loc = is_object($src); + if ($is_loc) { + $txt = $src->GetSrc(); + } else { + $txt =& $src; + } + + $opendoc = 'ReplaceSrc($txt); + } + + } + + +} + +/** + * clsTbsXmlLoc + * Wrapper to search and replace in XML entities. + * The object represents only the opening tag until method FindEndTag() is called. + * Then is represents the complete entity. + */ +class clsTbsXmlLoc { + + public $PosBeg; // Position of the first char ('<') of the element. + public $PosEnd; // Position of the char '>' of the start tag or the end tag, depending on whether the end tag has beend seached or not ($pET_PosBeg === false). + public $SelfClosing; // null|false|true, null means unknown. + public $Txt; // (by reference) Source of the contents where the locator is placed. + public $Name = ''; + public $Exists; // False means it is a phantom element + + public $pST_PosEnd = false; // Position of the end of the start tag ('>') + public $pST_Src = false; // Cached source of the start tag, false if not cached + public $pET_PosBeg = false; // Position of the begining of the end tag. False means the end tag has not been searched. + + public $Parent = false; // Parent object + + // For relative mode + public $rel_Txt = false; + public $rel_PosBeg = false; + public $rel_Len = false; + + // PHP 8.2 Compatibility + private $pST_PosBeg; + public $xlsxFileIdx; + + /** + * Search a start tag of an element in the TXT contents, and return an object if it is found. + * Instead of a TXT content, it can be an object of the class. Thus, the object is linked to a copy + * of the source of the parent element. The parent element can receive the changes of the object using method UpdateParent(). + */ + static function FindStartTag(&$TxtOrObj, $Tag, $PosBeg, $Forward=true) { + + if (is_object($TxtOrObj)) { + $TxtOrObj->FindEndTag(); + $Txt = $TxtOrObj->GetSrc(); + if ($Txt===false) return false; + $Parent = &$TxtOrObj; + } else { + $Txt = &$TxtOrObj; + $Parent = false; + } + + $PosBeg = clsTinyButStrong::f_Xml_FindTagStart($Txt, $Tag, true , $PosBeg, $Forward, true); + if ($PosBeg===false) return false; + + return new static($Txt, $Tag, $PosBeg, null, $Parent); + + } + + /** + * Search a start tag by the prefix of the element. + * @param string $TagPrefix The prefix of the tag. Empty string accepeted. + * @return false|object The found object will have its real tag name. + */ + static function FindStartTagByPrefix(&$Txt, $TagPrefix, $PosBeg, $Forward=true) { + + $x = '<'.$TagPrefix; + $xl = strlen($x); + + if ($Forward) { + $PosBeg = strpos($Txt, $x, $PosBeg); + } else { + $PosBeg = strrpos(substr($Txt, 0, $PosBeg+2), $x); + } + if ($PosBeg===false) return false; + + // Read the actual tag name + $Tag = $TagPrefix; + $p = $PosBeg + $xl; + do { + $z = substr($Txt,$p,1); + if ( ($z!==' ') && ($z!=="\r") && ($z!=="\n") && ($z!=='>') && ($z!=='/') ) { + $Tag .= $z; + $p++; + } else { + $p = false; + } + } while ($p!==false); + + return new static($Txt, $Tag, $PosBeg); + + } + + // Search an element in the TXT contents, and return an object if it's found. + static function FindElement(&$TxtOrObj, $Tag, $PosBeg, $Forward=true) { + + $XmlLoc = static::FindStartTag($TxtOrObj, $Tag, $PosBeg, $Forward); + if ($XmlLoc===false) return false; + + $XmlLoc->FindEndTag(); + return $XmlLoc; + + } + + /** + * Search a start tag in the TXT contents which has the asked attribute. + * Note that the element found has an unknown name until FindEndTag() is called. + * The function does check if the attribute is inside an XML element. + * @param string &$Txt The source to search into. + * @param string $Att The attribute name of full definition to search. Example: 'visible' or 'visible="1"' + * @param integer $PosBeg The offset position of the search. + * @param boolean $Forward (optional) Indicate the direction of the search. + * @return false|object + */ + static function FindStartTagHavingAtt(&$Txt, $Att, $PosBeg, $Forward=true) { + + $p = $PosBeg - (($Forward) ? 1 : -1); + $x = (strpos($Att, '=')===false) ? (' '.$Att.'="') : (' '.$Att); // get the item more precise if not yet done + $search = true; + + do { + if ($Forward) { + $p = strpos($Txt, $x, $p+1); + } else { + $p = strrpos(substr($Txt, 0, $p+1), $x); + } + if ($p===false) return false; + // Seearch for the bound of an element. + do { + $p = $p - 1; + if ($p<0) return false; + $z = $Txt[$p]; + } while ( ($z!=='<') && ($z!=='>') ); + // If the bound is an opening tag, then the attribute is inside, otherwise we search the next item. + if ($z==='<') $search = false; + } while ($search); + + return new static($Txt, '', $p); + + } + + /** + * Search an element in the TXT contents which has the asked attribute, and return an object if it is found. + * @param string &$Txt The source to search into. + * @param string $Att The attribute name of full definition to search. Example: 'visible' or 'visible="1"' + * @param integer $PosBeg The offset position of the search. + * @param boolean $Forward (optional) Indicate the direction of the search. + * @return false|object + */ + static function FindElementHavingAtt(&$Txt, $Att, $PosBeg, $Forward=true) { + + $XmlLoc = static::FindStartTagHavingAtt($Txt, $Att, $PosBeg, $Forward); + if ($XmlLoc===false) return false; + + $XmlLoc->FindEndTag(); + + return $XmlLoc; + + } + + /** + * Create an instance with a phantom element. + * A phatom element has a position but no contents, its length is 0. + * It can be usefull in order to prepare some modifications that will be precised later. + */ + static function CreatePhantomElement(&$TxtOrObj, $PosBeg) { + + if (is_object($TxtOrObj)) { + $TxtOrObj->FindEndTag(); + $Txt = $TxtOrObj->GetSrc(); + if ($Txt===false) return false; + $Parent = &$TxtOrObj; + } else { + $Txt = &$TxtOrObj; + $Parent = false; + } + + $Name = ''; + $SelfClosing = null; + $Exists = false; + + $XmlLoc = new static($Txt, $Name, $PosBeg, $SelfClosing, $Parent, $Exists); + + return $XmlLoc; + + } + + // Create an instance with the given parameters + function __construct(&$Txt, $Name, $PosBeg, $SelfClosing = null, $Parent = false, $Exists = true) { + + $this->Txt = &$Txt; + $this->Name = $Name; + $this->PosBeg = $PosBeg; + $this->Exists = $Exists; + if ($Exists) { + $this->PosEnd = strpos($Txt, '>', $PosBeg); + if ($this->PosEnd===false) $this->PosEnd = strlen($Txt)-1; // should no happen but avoid errors + } else { + $this->PosEnd = $PosBeg - 1; + } + $this->pST_PosEnd = $this->PosEnd; + $this->SelfClosing = $SelfClosing; + $this->Parent = $Parent; + } + + // Return an array of (val_pos, val_len, very_sart, very_len) of the attribute. Return false if the attribute is not found. + // Positions are relative to $this->PosBeg. + // This method is lazy because it assumes the attribute is separated by a space and its value is delimited by double-quote. + function _GetAttValPos($Att) { + if ($this->pST_Src===false) $this->pST_Src = substr($this->Txt, $this->PosBeg, $this->pST_PosEnd - $this->PosBeg + 1 ); + $a = ' '.$Att.'="'; + $p0 = strpos($this->pST_Src, $a); + if ($p0!==false) { + $p1 = $p0 + strlen($a); + $p2 = strpos($this->pST_Src, '"', $p1); + if ($p2!==false) return array($p1, $p2-$p1, $p0, $p2-$p0+1); + } + return false; + } + + // Update positions when attributes of the start tag has been upated. + function _ApplyDiffFromStart($Diff) { + $this->pST_PosEnd += $Diff; + $this->pST_Src = false; + if ($this->pET_PosBeg!==false) $this->pET_PosBeg += $Diff; + $this->PosEnd += $Diff; + } + + // Update all positions. + function _ApplyDiffToAll($Diff) { + $this->PosBeg += $Diff; + $this->PosEnd += $Diff; + $this->pST_PosEnd += $Diff; + if ($this->pET_PosBeg!==false) $this->pET_PosBeg += $Diff; + } + + /** + * Convert a self-closing entity to a start+end entity if needed. + * @param string $inner The inner content to insert. + */ + function _ConvertToCouple($inner) { + $end = '>' . $inner . 'FindName() . '>'; + $this->Txt = substr_replace($this->Txt, $end, $this->PosEnd - 1, 2); + $this->SelfClosing = false; + $this->pST_PosEnd = $this->PosEnd - 1; + $this->pET_PosBeg = $this->pST_PosEnd + strlen($inner) + 1; + $this->PosEnd = $this->pST_PosEnd + strlen($end) - 1; + } + + // Return true is the ending position is a self-closing. + function _SelfClosing($PosEnd) { + return (substr($this->Txt, $PosEnd-1, 1)=='/'); + } + + /** + * Return the outer length of the locator. + * That is the length between including '<' and '>'. + * It may include only the start tag if the end tag has never been searched. + * + * @return integer + */ + function GetLen() { + return $this->PosEnd - $this->PosBeg + 1; + } + + /** + * Return the outer source of the locator. + * That is the string including '<' and '>'. + * It may include only the start tag if the end tag has never been searched. + * + * @return string + */ + function GetSrc() { + return substr($this->Txt, $this->PosBeg, $this->GetLen() ); + } + + /** + * Replace the source of the locator in the TXT contents. + * Update the locator's ending position. + * Too complicated to update other information, given that it can be deleted. + * + * @param string $new New full source of the locator. For exemple 'Hello'. + * Empty string ('') means the locator is deleted. + * + * @return void + */ + function ReplaceSrc($new) { + $len = $this->GetLen(); // avoid PHP error : Strict Standards: Only variables should be passed by reference + $this->Txt = substr_replace($this->Txt, $new, $this->PosBeg, $len); + $diff = strlen($new) - $len; + $this->PosEnd += $diff; + $this->pST_Src = false; + if ($new==='') { + $this->pST_PosBeg = false; + $this->pST_PosEnd = false; + $this->pET_PosBeg = false; + $this->Exists = false; + } else { + $this->pST_PosEnd += $diff; // CAUTION: may be wrong if attributes has changed + if ($this->pET_PosBeg!==false) $this->pET_PosBeg += $diff; // CAUTION: right only if the tag name is the same + $this->Exists = true; + } + } + + /** + * Return the position for appending at the end of the inner contents (that is the string between start and end tags). + * Return false if SelfClosing. + * + * @return integer|false + */ + function GetInnerAppendPos() { + return $this->pET_PosBeg; + } + + /** + * Return the start of the inner content. + * Return false if SelfClosing. + * + * @return integer|false + */ + function GetInnerStart() { + return ($this->pST_PosEnd===false) ? false : $this->pST_PosEnd + 1; + } + + /** + * Return the length of the inner content, or false if it's a self-closing tag. + * Assume FindEndTag() is previously called. + * Return false if SelfClosing. + * + * @return integer|false + */ + function GetInnerLen() { + return ($this->pET_PosBeg===false) ? false : $this->pET_PosBeg - $this->pST_PosEnd - 1; + } + + /** + * Return the contents of the inner content, or false if it's a self-closing tag + * Assume FindEndTag() is previously called. + * Return false if SelfClosing. + * + * @return string|false + */ + function GetInnerSrc() { + return ($this->pET_PosBeg===false) ? false : substr($this->Txt, $this->pST_PosEnd + 1, $this->pET_PosBeg - $this->pST_PosEnd - 1 ); + } + + /** + * Replace the inner source of the locator. + * Update the locator's positions. + * Assume FindEndTag() is previously called. + * Convert a self-closing entity to a start+end entity if needed. + * + * @return void + */ + function ReplaceInnerSrc($new) { + if ($this->SelfClosing) { + $this->_ConvertToCouple($new); + } else { + $len = $this->GetInnerLen(); + if ($len===false) return false; + $this->Txt = substr_replace($this->Txt, $new, $this->pST_PosEnd + 1, $len); + $this->PosEnd += strlen($new) - $len; + $this->pET_PosBeg += strlen($new) - $len; + } + } + + /** + * Append a contents at the end of the inner source. + * + * @param string $add The string to add. + * + * @return void + */ + function AppendInnerSrc($add) { + if ($this->SelfClosing) { + $this->_ConvertToCouple($add); + } else { + $this->Txt = substr_replace($this->Txt, $add, $this->pET_PosBeg, 0); + $this->PosEnd += strlen($add); + $this->pET_PosBeg += strlen($add); + } + } + + /** + * Update the parent object, if any. + * + * @param boolean $Cascading (optional, false by default) Also update all the parents of the tree. + * + * @return void + */ + function UpdateParent($Cascading = false) { + if ($this->Parent) { + $this->Parent->ReplaceSrc($this->Txt); + if ($Cascading) $this->Parent->UpdateParent($Cascading); + } + } + + /** + * Get an attribute's value. Or false if the attribute is not found. + * It's a lazy way because the attribute is searched with the patern {attribute="value" } + * + * @param string $Att The name of the attribute. + * + * @return string + */ + function GetAttLazy($Att) { + $z = $this->_GetAttValPos($Att); + if ($z===false) return false; + return substr($this->pST_Src, $z[0], $z[1]); + } + + /** + * Replace an attribute's value. Can eventually create the attribute if missing. + * + * @param string $Att The name of the attribute. + * @param string $Value The new value of the attribute. You have to protect the contents before. + * @param boolean $AddIfMissing (optional, false by default) True means the attribute is added if missing. + * + * @return boolean Return True if the value has been replaced or inserted. + */ + function ReplaceAtt($Att, $Value, $AddIfMissing = false) { + + $Value = ''.$Value; + + $z = $this->_GetAttValPos($Att); + if ($z===false) { + if ($AddIfMissing) { + // Add the attribute + $Value = ' '.$Att.'="'.$Value.'"'; + $pi = $this->pST_PosEnd; + if ($this->_SelfClosing($pi)) $pi--; + $z = array($pi - $this->PosBeg, 0); + } else { + return false; + } + } + + $this->Txt = substr_replace($this->Txt, $Value, $this->PosBeg + $z[0], $z[1]); + + // update info + $this->_ApplyDiffFromStart(strlen($Value) - $z[1]); + + return true; + + } + + /** + * Delete the element with or without the content. + * + * @param boolean $Contents (optional, true by default) If False, then only the inner contents (excluding start and end tags) is deleted. + * + * @return void + */ + function Delete($Contents = true) { + $this->FindEndTag(); + if ($Contents || $this->SelfClosing) { + $this->ReplaceSrc(''); + } else { + $inner = $this->GetInnerSrc(); + $this->ReplaceSrc($inner); + } + } + + /** + * Return true if the attribute existed and is deleted, otherwise return false. + * + * @return boolean + */ + function DeleteAtt($Att) { + $z = $this->_GetAttValPos($Att); + if ($z===false) return false; + $this->Txt = substr_replace($this->Txt, '', $this->PosBeg + $z[2], $z[3]); + $this->_ApplyDiffFromStart( - $z[3]); + return true; + } + + /** + * Find and return the name of the element + * + * @return string + */ + function FindName() { + if ( ($this->Name==='') && $this->Exists ) { + $p = $this->PosBeg; + do { + $p++; + $z = $this->Txt[$p]; + } while ( ($z!==' ') && ($z!=="\r") && ($z!=="\n") && ($z!=='>') && ($z!=='/') ); + $this->Name = substr($this->Txt, $this->PosBeg + 1, $p - $this->PosBeg - 1); + } + return $this->Name; + } + + /** + * Find the ending tag of the entity. + * The result is put in cache for other calls. + * + * @param boolean $Encaps (optional, true by default) Indicates if the element can be self encapsulated (like
). + * + * @return boolean Return True if the end is found, or False otherwise. + */ + function FindEndTag($Encaps=false) { + if (is_null($this->SelfClosing)) { + $pe = $this->PosEnd; + $SelfClosing = $this->_SelfClosing($pe); + if (!$SelfClosing) { + if ($Encaps) { + $loc = clsTinyButStrong::f_Xml_FindTag($this->Txt , $this->FindName(), null, $pe, true, -1, false, false); + if ($loc===false) return false; + $this->pET_PosBeg = $loc->PosBeg; + $this->PosEnd = $loc->PosEnd; + } else { + $pe = clsTinyButStrong::f_Xml_FindTagStart($this->Txt, $this->FindName(), false, $pe, true , true); + if ($pe===false) return false; + $this->pET_PosBeg = $pe; + $pe = strpos($this->Txt, '>', $pe); + if ($pe===false) return false; + $this->PosEnd = $pe; + } + } + $this->SelfClosing = $SelfClosing; + } + return true; + } + + /** + * Switch a normal locator to a relative locator. + * A relative locator is isolated : it has no text before and no text after. + * Relative locators are useful to save time in search and replace within the locator. + */ + function switchToRelative() { + $this->FindEndTag(); + // Save info + $this->rel_Txt = &$this->Txt; + $this->rel_PosBeg = $this->PosBeg; + $this->rel_Len = $this->GetLen(); + // Change the univers + $src = $this->GetSrc(); + $this->Txt = &$src; + // Change positions + $this->_ApplyDiffToAll(-$this->PosBeg); + } + + /** + * To use after switchToRelative(): save modification to the normal contents and update positions. + */ + function switchToNormal() { + // Save info + $src = $this->GetSrc(); + $this->Txt = &$this->rel_Txt; + $x = false; + $this->rel_Txt = &$x; + $this->Txt = substr_replace($this->Txt, $src, $this->rel_PosBeg, $this->rel_Len); + $this->_ApplyDiffToAll(+$this->rel_PosBeg); + $this->rel_PosBeg = false; + $this->rel_Len = false; + } + +} + +class clsTbsXmlCellReader extends clsTbsXmlLoc { + + public $RepeatIdx; + public $RepeatMax; + public $RowOk; + public $cellCol; + public $cellRow; + public $CellLst; + +} + +/* +TbsZip version 2.17 +Date : 2023-09-16 +Author : Skrol29 (email: http://www.tinybutstrong.com/onlyyou.html) +Licence : LGPL +This class is independent from any other classes and has been originally created for the OpenTbs plug-in +for TinyButStrong Template Engine (TBS). OpenTbs makes TBS able to merge OpenOffice and Ms Office documents. +Visit http://www.tinybutstrong.com +*/ + +define('TBSZIP_DOWNLOAD',1); // download (default) +define('TBSZIP_NOHEADER',4); // option to use with DOWNLOAD: no header is sent +define('TBSZIP_FILE',8); // output to file , or add from file +define('TBSZIP_STRING',32); // output to string, or add from string + +class clsTbsZip { + + public $Meth8Ok; + public $DisplayError; + public $ArchFile; + public $Error; + + // Compatibility PHP 8.2 + public $ArchHnd; + public $ArchIsNew; + public $CdEndPos; + public $CdPos; + public $CdInfo; + public $ArchIsStream; + public $CdFileLst; + public $CdFileNbr; + public $CdFileByName; + public $VisFileLst; + public $LastReadComp; + public $LastReadIdx; + public $ReplInfo; + public $ReplByPos; + public $AddInfo; + public $OutputMode; + public $OutputHandle; + public $OutputSrc; + + function __construct() { + $this->Meth8Ok = extension_loaded('zlib'); // check if Zlib extension is available. This is need for compress and uncompress with method 8. + $this->DisplayError = true; + $this->ArchFile = ''; + $this->Error = false; + } + + /** + * Create a new virtual empty archive, the name will be the default name when the archive is flushed. + */ + function CreateNew($ArchName='new.zip') { + if (!isset($this->Meth8Ok)) $this->__construct(); // for PHP 4 compatibility + $this->Close(); // note that $this->ArchHnd is set to false here + $this->Error = false; + $this->ArchFile = $ArchName; + $this->ArchIsNew = true; + $bin = 'PK'.chr(05).chr(06).str_repeat(chr(0), 18); + $this->CdEndPos = strlen($bin) - 4; + $this->CdInfo = array('disk_num_curr'=>0, 'disk_num_cd'=>0, 'file_nbr_curr'=>0, 'file_nbr_tot'=>0, 'l_cd'=>0, 'p_cd'=>0, 'l_comm'=>0, 'v_comm'=>'', 'bin'=>$bin); + $this->CdPos = $this->CdInfo['p_cd']; + } + + /** + * Open the zip archive + */ + function Open($ArchFile, $UseIncludePath=false) { + + if (!isset($this->Meth8Ok)) $this->__construct(); // for PHP 4 compatibility + $this->Close(); // close handle and init info + $this->Error = false; + $this->ArchIsNew = false; + $this->ArchIsStream = (is_resource($ArchFile) && (get_resource_type($ArchFile)=='stream')); + if ($this->ArchIsStream) { + $info = stream_get_meta_data($ArchFile); + if (isset($info['uri'])) { + $this->ArchFile = $info['uri']; + } else { + $this->ArchFile = 'from_stream.zip'; + } + $this->ArchHnd = $ArchFile; + } else { + // open the file + $this->ArchFile = $ArchFile; + $this->ArchHnd = fopen($ArchFile, 'rb', $UseIncludePath); + } + $ok = !($this->ArchHnd===false); + if ($ok) $ok = $this->CentralDirRead(); + return $ok; + } + + function Close() { + if (isset($this->ArchHnd) and ($this->ArchHnd!==false)) fclose($this->ArchHnd); + $this->ArchFile = ''; + $this->ArchHnd = false; + $this->CdInfo = array(); + $this->CdFileLst = array(); + $this->CdFileNbr = 0; + $this->CdFileByName = array(); + $this->VisFileLst = array(); + $this->ArchCancelModif(); + } + + function ArchCancelModif() { + $this->LastReadComp = false; // compression of the last read file (1=compressed, 0=stored not compressed, -1= stored compressed but read uncompressed) + $this->LastReadIdx = false; // index of the last file read + $this->ReplInfo = array(); + $this->ReplByPos = array(); + $this->AddInfo = array(); + } + + function FileAdd($Name, $Data, $DataType=TBSZIP_STRING, $Compress=true) { + + if ($Data===false) return $this->FileCancelModif($Name, false); // Cancel a previously added file + + // Save information for adding a new file into the archive + $Diff = 30 + 46 + 2*strlen($Name); // size of the header + cd info + $Ref = $this->_DataCreateNewRef($Data, $DataType, $Compress, $Diff, $Name); + if ($Ref===false) return false; + $Ref['name'] = $Name; + $this->AddInfo[] = $Ref; + return $Ref['res']; + + } + + function CentralDirRead() { + $cd_info = 'PK'.chr(05).chr(06); // signature of the Central Directory + $cd_pos = -22; + $this->_MoveTo($cd_pos, SEEK_END); + $b = $this->_ReadData(4); + if ($b===$cd_info) { + $this->CdEndPos = ftell($this->ArchHnd) - 4; + } else { + $p = $this->_FindCDEnd($cd_info); + //echo 'p='.var_export($p,true); exit; + if ($p===false) { + return $this->RaiseError('The End of Central Directory Record is not found.'); + } else { + $this->CdEndPos = $p; + $this->_MoveTo($p+4); + } + } + $this->CdInfo = $this->CentralDirRead_End($cd_info); + $this->CdFileLst = array(); + $this->CdFileNbr = $this->CdInfo['file_nbr_curr']; + $this->CdPos = $this->CdInfo['p_cd']; + + if ($this->CdFileNbr<=0) return $this->RaiseError('No header found in the Central Directory.'); + if ($this->CdPos<=0) return $this->RaiseError('No position found for the Central Directory.'); + + $this->_MoveTo($this->CdPos); + for ($i=0;$i<$this->CdFileNbr;$i++) { + $x = $this->CentralDirRead_File($i); + if ($x!==false) { + $this->CdFileLst[$i] = $x; + $this->CdFileByName[$x['v_name']] = $i; + } + } + return true; + } + + function CentralDirRead_End($cd_info) { + $b = $cd_info.$this->_ReadData(18); + $x = array(); + $x['disk_num_curr'] = $this->_GetDec($b,4,2); // number of this disk + $x['disk_num_cd'] = $this->_GetDec($b,6,2); // number of the disk with the start of the central directory + $x['file_nbr_curr'] = $this->_GetDec($b,8,2); // total number of entries in the central directory on this disk + $x['file_nbr_tot'] = $this->_GetDec($b,10,2); // total number of entries in the central directory + $x['l_cd'] = $this->_GetDec($b,12,4); // size of the central directory + $x['p_cd'] = $this->_GetDec($b,16,4); // position of start of central directory with respect to the starting disk number + $x['l_comm'] = $this->_GetDec($b,20,2); // .ZIP file comment length + $x['v_comm'] = $this->_ReadData($x['l_comm']); // .ZIP file comment + $x['bin'] = $b.$x['v_comm']; + return $x; + } + + function CentralDirRead_File($idx) { + + $b = $this->_ReadData(46); + + $x = $this->_GetHex($b,0,4); + if ($x!=='h:02014b50') return $this->RaiseError("Signature of Central Directory Header #".$idx." (file information) expected but not found at position ".$this->_TxtPos(ftell($this->ArchHnd) - 46)."."); + + $x = array(); + $x['vers_used'] = $this->_GetDec($b,4,2); + $x['vers_necess'] = $this->_GetDec($b,6,2); + $x['purp'] = $this->_GetBin($b,8,2); + $x['meth'] = $this->_GetDec($b,10,2); + $x['time'] = $this->_GetDec($b,12,2); + $x['date'] = $this->_GetDec($b,14,2); + $x['crc32'] = $this->_GetDec($b,16,4); + $x['l_data_c'] = $this->_GetDec($b,20,4); + $x['l_data_u'] = $this->_GetDec($b,24,4); + $x['l_name'] = $this->_GetDec($b,28,2); + $x['l_fields'] = $this->_GetDec($b,30,2); + $x['l_comm'] = $this->_GetDec($b,32,2); + $x['disk_num'] = $this->_GetDec($b,34,2); + $x['int_file_att'] = $this->_GetDec($b,36,2); + $x['ext_file_att'] = $this->_GetDec($b,38,4); + $x['p_loc'] = $this->_GetDec($b,42,4); + $x['v_name'] = $this->_ReadData($x['l_name']); + $x['v_fields'] = $this->_ReadData($x['l_fields']); + $x['v_comm'] = $this->_ReadData($x['l_comm']); + + $x['bin'] = $b.$x['v_name'].$x['v_fields'].$x['v_comm']; + + return $x; + } + + function RaiseError($Msg) { + if ($this->DisplayError) { + if (PHP_SAPI==='cli') { + echo get_class($this).' ERROR with the zip archive: '.$Msg."\r\n"; + } else { + echo ''.get_class($this).' ERROR with the zip archive: '.$Msg.'
'."\r\n"; + } + } + $this->Error = $Msg; + return false; + } + + function Debug($FileHeaders=false) { + + $this->DisplayError = true; + + if ($FileHeaders) { + // Calculations first in order to have error messages before other information + $idx = 0; + $pos = 0; + $pos_stop = $this->CdInfo['p_cd']; + $this->_MoveTo($pos); + while ( ($pos<$pos_stop) && ($ok = $this->_ReadFile($idx,false)) ) { + $this->VisFileLst[$idx]['p_this_header (debug_mode only)'] = $pos; + $pos = ftell($this->ArchHnd); + $idx++; + } + } + + $nl = "\r\n"; + echo "
";
+
+		echo "-------------------------------".$nl;
+		echo "End of Central Directory record".$nl;
+		echo "-------------------------------".$nl;
+		print_r($this->DebugArray($this->CdInfo));
+
+		echo $nl;
+		echo "-------------------------".$nl;
+		echo "Central Directory headers".$nl;
+		echo "-------------------------".$nl;
+		print_r($this->DebugArray($this->CdFileLst));
+
+		if ($FileHeaders) {
+			echo $nl;
+			echo "------------------".$nl;
+			echo "Local File headers".$nl;
+			echo "------------------".$nl;
+			print_r($this->DebugArray($this->VisFileLst));
+		}
+
+		echo "
"; + + } + + function DebugArray($arr) { + foreach ($arr as $k=>$v) { + if (is_array($v)) { + $arr[$k] = $this->DebugArray($v); + } elseif (substr($k,0,2)=='p_') { + $arr[$k] = $this->_TxtPos($v); + } + } + return $arr; + } + + function FileExists($NameOrIdx) { + return ($this->FileGetIdx($NameOrIdx)!==false); + } + + /** + * Check if a file name, or a file index exists in the Central Directory, and return its index + */ + function FileGetIdx($NameOrIdx) { + if (is_string($NameOrIdx)) { + if (isset($this->CdFileByName[$NameOrIdx])) { + return $this->CdFileByName[$NameOrIdx]; + } else { + return false; + } + } else { + if (isset($this->CdFileLst[$NameOrIdx])) { + return $NameOrIdx; + } else { + return false; + } + } + } + + /** + * Check if a file name exists in the list of file to add, and return its index + */ + function FileGetIdxAdd($Name) { + if (!is_string($Name)) return false; + $idx_lst = array_keys($this->AddInfo); + foreach ($idx_lst as $idx) { + if ($this->AddInfo[$idx]['name']===$Name) return $idx; + } + return false; + } + + function FileRead($NameOrIdx, $Uncompress=true) { + + $this->LastReadComp = false; // means the file is not found + $this->LastReadIdx = false; + + $idx = $this->FileGetIdx($NameOrIdx); + if ($idx===false) return $this->RaiseError('File "'.$NameOrIdx.'" is not found in the Central Directory.'); + + $pos = $this->CdFileLst[$idx]['p_loc']; + $this->_MoveTo($pos); + + $this->LastReadIdx = $idx; // Can be usefull to get the idx + + $Data = $this->_ReadFile($idx, true); + + // Manage uncompression + $Comp = 1; // means the contents stays compressed + $meth = $this->CdFileLst[$idx]['meth']; + if ($meth==8) { + if ($Uncompress) { + if ($this->Meth8Ok) { + $Data = gzinflate($Data); + $Comp = -1; // means uncompressed + } else { + $this->RaiseError('Unable to uncompress file "'.$NameOrIdx.'" because extension Zlib is not installed.'); + } + } + } elseif($meth==0) { + $Comp = 0; // means stored without compression + } else { + if ($Uncompress) $this->RaiseError('Unable to uncompress file "'.$NameOrIdx.'" because it is compressed with method '.$meth.'.'); + } + $this->LastReadComp = $Comp; + + return $Data; + + } + + /** + * Read the file header (and maybe the data ) in the archive, assuming the cursor in at a new file position + */ + function _ReadFile($idx, $ReadData) { + + $b = $this->_ReadData(30); + + $x = $this->_GetHex($b,0,4); + if ($x!=='h:04034b50') return $this->RaiseError("Signature of Local File Header #".$idx." (data section) expected but not found at position ".$this->_TxtPos(ftell($this->ArchHnd)-30)."."); + + $x = array(); + $x['vers'] = $this->_GetDec($b,4,2); + $x['purp'] = $this->_GetBin($b,6,2); + $x['meth'] = $this->_GetDec($b,8,2); + $x['time'] = $this->_GetDec($b,10,2); + $x['date'] = $this->_GetDec($b,12,2); + $x['crc32'] = $this->_GetDec($b,14,4); + $x['l_data_c'] = $this->_GetDec($b,18,4); + $x['l_data_u'] = $this->_GetDec($b,22,4); + $x['l_name'] = $this->_GetDec($b,26,2); + $x['l_fields'] = $this->_GetDec($b,28,2); + $x['v_name'] = $this->_ReadData($x['l_name']); + $x['v_fields'] = $this->_ReadData($x['l_fields']); + + $x['bin'] = $b.$x['v_name'].$x['v_fields']; + + // Read Data + if (isset($this->CdFileLst[$idx])) { + $len_cd = $this->CdFileLst[$idx]['l_data_c']; + if ($x['l_data_c']==0) { + // Sometimes, the size is not specified in the local information. + $len = $len_cd; + } else { + $len = $x['l_data_c']; + if ($len!=$len_cd) { + //echo "TbsZip Warning: Local information for file #".$idx." says len=".$len.", while Central Directory says len=".$len_cd."."; + } + } + } else { + $len = $x['l_data_c']; + if ($len==0) $this->RaiseError("File Data #".$idx." cannt be read because no length is specified in the Local File Header and its Central Directory information has not been found."); + } + + if ($ReadData) { + $Data = $this->_ReadData($len); + } else { + $this->_MoveTo($len, SEEK_CUR); + } + + // Description information + $desc_ok = ($x['purp'][2+3]=='1'); + if ($desc_ok) { + $b = $this->_ReadData(12); + $s = $this->_GetHex($b,0,4); + $d = 0; + // the specification says the signature may or may not be present + if ($s=='h:08074b50') { + $b .= $this->_ReadData(4); + $d = 4; + $x['desc_bin'] = $b; + $x['desc_sign'] = $s; + } else { + $x['desc_bin'] = $b; + } + $x['desc_crc32'] = $this->_GetDec($b,0+$d,4); + $x['desc_l_data_c'] = $this->_GetDec($b,4+$d,4); + $x['desc_l_data_u'] = $this->_GetDec($b,8+$d,4); + } + + // Save file info without the data + $this->VisFileLst[$idx] = $x; + + // Return the info + if ($ReadData) { + return $Data; + } else { + return true; + } + + } + + /** + * Store replacement information. + */ + function FileReplace($NameOrIdx, $Data, $DataType=TBSZIP_STRING, $Compress=true) { + + $idx = $this->FileGetIdx($NameOrIdx); + if ($idx===false) return $this->RaiseError('File "'.$NameOrIdx.'" is not found in the Central Directory.'); + + $pos = $this->CdFileLst[$idx]['p_loc']; + + if ($Data===false) { + // file to delete + $this->ReplInfo[$idx] = false; + $Result = true; + } else { + // file to replace + $Diff = - $this->CdFileLst[$idx]['l_data_c']; + $Ref = $this->_DataCreateNewRef($Data, $DataType, $Compress, $Diff, $NameOrIdx); + if ($Ref===false) return false; + $this->ReplInfo[$idx] = $Ref; + $Result = $Ref['res']; + } + + $this->ReplByPos[$pos] = $idx; + + return $Result; + + } + + /** + * Return the state of the file. + * @return {string} 'u'=unchanged, 'm'=modified, 'd'=deleted, 'a'=added, false=unknown + */ + function FileGetState($NameOrIdx) { + + $idx = $this->FileGetIdx($NameOrIdx); + if ($idx===false) { + $idx = $this->FileGetIdxAdd($NameOrIdx); + if ($idx===false) { + return false; + } else { + return 'a'; + } + } elseif (isset($this->ReplInfo[$idx])) { + if ($this->ReplInfo[$idx]===false) { + return 'd'; + } else { + return 'm'; + } + } else { + return 'u'; + } + + } + + /** + * Cancel added, modified or deleted modifications on a file in the archive. + * @return integer The number of cancellations. + */ + function FileCancelModif($NameOrIdx, $ReplacedAndDeleted=true) { + + $nbr = 0; + + if ($ReplacedAndDeleted) { + // replaced or deleted files + $idx = $this->FileGetIdx($NameOrIdx); + if ($idx!==false) { + if (isset($this->ReplInfo[$idx])) { + $pos = $this->CdFileLst[$idx]['p_loc']; + unset($this->ReplByPos[$pos]); + unset($this->ReplInfo[$idx]); + $nbr++; + } + } + } + + // added files + $idx = $this->FileGetIdxAdd($NameOrIdx); + if ($idx!==false) { + unset($this->AddInfo[$idx]); + $nbr++; + } + + return $nbr; + + } + + function Flush($Render=TBSZIP_DOWNLOAD, $File='', $ContentType='') { + + if ( ($File!=='') && ($this->ArchFile===$File) && ($Render==TBSZIP_FILE) ) { + $this->RaiseError('Method Flush() cannot overwrite the current opened archive: \''.$File.'\''); // this makes corrupted zip archives without PHP error. + return false; + } + + $ArchPos = 0; + $Delta = 0; + $FicNewPos = array(); + $DelLst = array(); // idx of deleted files + $DeltaCdLen = 0; // delta of the CD's size + + $now = time(); + $date = $this->_MsDos_Date($now); + $time = $this->_MsDos_Time($now); + + if (!$this->OutputOpen($Render, $File, $ContentType)) return false; + + // output modified zipped files and unmodified zipped files that are beetween them + ksort($this->ReplByPos); + foreach ($this->ReplByPos as $ReplPos => $ReplIdx) { + // output data from the zip archive which is before the data to replace + $this->OutputFromArch($ArchPos, $ReplPos); + // get current file information + if (!isset($this->VisFileLst[$ReplIdx])) $this->_ReadFile($ReplIdx, false); + $FileInfo =& $this->VisFileLst[$ReplIdx]; + $b1 = $FileInfo['bin']; + if (isset($FileInfo['desc_bin'])) { + $b2 = $FileInfo['desc_bin']; + } else { + $b2 = ''; + } + $info_old_len = strlen($b1) + $this->CdFileLst[$ReplIdx]['l_data_c'] + strlen($b2); // $FileInfo['l_data_c'] may have a 0 value in some archives + // get replacement information + $ReplInfo =& $this->ReplInfo[$ReplIdx]; + if ($ReplInfo===false) { + // The file is to be deleted + $Delta = $Delta - $info_old_len; // headers and footers are also deleted + $DelLst[$ReplIdx] = true; + } else { + // prepare the header of the current file + $this->_DataPrepare($ReplInfo); // get data from external file if necessary + $this->_PutDec($b1, $time, 10, 2); // time + $this->_PutDec($b1, $date, 12, 2); // date + $this->_PutDec($b1, $ReplInfo['crc32'], 14, 4); // crc32 + $this->_PutDec($b1, $ReplInfo['len_c'], 18, 4); // l_data_c + $this->_PutDec($b1, $ReplInfo['len_u'], 22, 4); // l_data_u + if ($ReplInfo['meth']!==false) $this->_PutDec($b1, $ReplInfo['meth'], 8, 2); // meth + // prepare the bottom description if the zipped file, if any + if ($b2!=='') { + $d = (strlen($b2)==16) ? 4 : 0; // offset because of the signature if any + $this->_PutDec($b2, $ReplInfo['crc32'], $d+0, 4); // crc32 + $this->_PutDec($b2, $ReplInfo['len_c'], $d+4, 4); // l_data_c + $this->_PutDec($b2, $ReplInfo['len_u'], $d+8, 4); // l_data_u + } + // output data + $this->OutputFromString($b1.$ReplInfo['data'].$b2); + unset($ReplInfo['data']); // save PHP memory + $Delta = $Delta + $ReplInfo['diff'] + $ReplInfo['len_c']; + } + // Update the delta of positions for zipped files which are physically after the currently replaced one + for ($i=0;$i<$this->CdFileNbr;$i++) { + if ($this->CdFileLst[$i]['p_loc']>$ReplPos) { + $FicNewPos[$i] = $this->CdFileLst[$i]['p_loc'] + $Delta; + } + } + // Update the current pos in the archive + $ArchPos = $ReplPos + $info_old_len; + } + + // Ouput all the zipped files that remain before the Central Directory listing + if ($this->ArchHnd!==false) $this->OutputFromArch($ArchPos, $this->CdPos); // ArchHnd is false if CreateNew() has been called + $ArchPos = $this->CdPos; + + // Output file to add + $AddNbr = count($this->AddInfo); + $AddDataLen = 0; // total len of added data (inlcuding file headers) + if ($AddNbr>0) { + $AddPos = $ArchPos + $Delta; // position of the start + $AddLst = array_keys($this->AddInfo); + foreach ($AddLst as $idx) { + $n = $this->_DataOuputAddedFile($idx, $AddPos); + $AddPos += $n; + $AddDataLen += $n; + } + } + + // Modifiy file information in the Central Directory for replaced files + $b2 = ''; + $old_cd_len = 0; + for ($i=0;$i<$this->CdFileNbr;$i++) { + $b1 = $this->CdFileLst[$i]['bin']; + $old_cd_len += strlen($b1); + if (!isset($DelLst[$i])) { + if (isset($FicNewPos[$i])) $this->_PutDec($b1, $FicNewPos[$i], 42, 4); // p_loc + if (isset($this->ReplInfo[$i])) { + $ReplInfo =& $this->ReplInfo[$i]; + $this->_PutDec($b1, $time, 12, 2); // time + $this->_PutDec($b1, $date, 14, 2); // date + $this->_PutDec($b1, $ReplInfo['crc32'], 16, 4); // crc32 + $this->_PutDec($b1, $ReplInfo['len_c'], 20, 4); // l_data_c + $this->_PutDec($b1, $ReplInfo['len_u'], 24, 4); // l_data_u + if ($ReplInfo['meth']!==false) $this->_PutDec($b1, $ReplInfo['meth'], 10, 2); // meth + } + $b2 .= $b1; + } + } + $this->OutputFromString($b2); + $ArchPos += $old_cd_len; + $DeltaCdLen = $DeltaCdLen + strlen($b2) - $old_cd_len; + + // Output until "end of central directory record" + if ($this->ArchHnd!==false) $this->OutputFromArch($ArchPos, $this->CdEndPos); // ArchHnd is false if CreateNew() has been called + + // Output file information of the Central Directory for added files + if ($AddNbr>0) { + $b2 = ''; + foreach ($AddLst as $idx) { + $b2 .= $this->AddInfo[$idx]['bin']; + } + $this->OutputFromString($b2); + $DeltaCdLen += strlen($b2); + } + + // Output "end of central directory record" + $b2 = $this->CdInfo['bin']; + $DelNbr = count($DelLst); + if ( ($AddNbr>0) || ($DelNbr>0) ) { + // total number of entries in the central directory on this disk + $n = $this->_GetDec($b2, 8, 2); + $this->_PutDec($b2, $n + $AddNbr - $DelNbr, 8, 2); + // total number of entries in the central directory + $n = $this->_GetDec($b2, 10, 2); + $this->_PutDec($b2, $n + $AddNbr - $DelNbr, 10, 2); + // size of the central directory + $n = $this->_GetDec($b2, 12, 4); + $this->_PutDec($b2, $n + $DeltaCdLen, 12, 4); + $Delta = $Delta + $AddDataLen; + } + $this->_PutDec($b2, $this->CdPos+$Delta , 16, 4); // p_cd (offset of start of central directory with respect to the starting disk number) + $this->OutputFromString($b2); + + $this->OutputClose(); + + return true; + + } + + // ---------------- + // output functions + // ---------------- + + function OutputOpen($Render, $File, $ContentType) { + + if (($Render & TBSZIP_FILE)==TBSZIP_FILE) { + $this->OutputMode = TBSZIP_FILE; + if (''.$File=='') $File = basename($this->ArchFile).'.zip'; + $this->OutputHandle = @fopen($File, 'w'); + if ($this->OutputHandle===false) { + return $this->RaiseError('Method Flush() cannot overwrite the target file \''.$File.'\'. This may not be a valid file path or the file may be locked by another process or because of a denied permission.'); + } + } elseif (($Render & TBSZIP_STRING)==TBSZIP_STRING) { + $this->OutputMode = TBSZIP_STRING; + $this->OutputSrc = ''; + } elseif (($Render & TBSZIP_DOWNLOAD)==TBSZIP_DOWNLOAD) { + $this->OutputMode = TBSZIP_DOWNLOAD; + // Output the file + if (''.$File=='') $File = basename($this->ArchFile); + if (($Render & TBSZIP_NOHEADER)==TBSZIP_NOHEADER) { + } else { + header ('Pragma: no-cache'); + if ($ContentType!='') header ('Content-Type: '.$ContentType); + header('Content-Disposition: attachment; filename="'.$File.'"'); + header('Expires: 0'); + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + header('Cache-Control: public'); + header('Content-Description: File Transfer'); + header('Content-Transfer-Encoding: binary'); + $Len = $this->_EstimateNewArchSize(); + if ($Len!==false) header('Content-Length: '.$Len); + } + } else { + return $this->RaiseError('Method Flush is called with a unsupported render option.'); + } + + return true; + + } + + function OutputFromArch($pos, $pos_stop) { + $len = $pos_stop - $pos; + if ($len<0) return; + $this->_MoveTo($pos); + $block = 1024; + while ($len>0) { + $l = min($len, $block); + $x = $this->_ReadData($l); + $this->OutputFromString($x); + $len = $len - $l; + } + unset($x); + } + + function OutputFromString($data) { + if ($this->OutputMode===TBSZIP_DOWNLOAD) { + echo $data; // donwload + } elseif ($this->OutputMode===TBSZIP_STRING) { + $this->OutputSrc .= $data; // to string + } elseif (TBSZIP_FILE) { + fwrite($this->OutputHandle, $data); // to file + } + } + + function OutputClose() { + if ( ($this->OutputMode===TBSZIP_FILE) && ($this->OutputHandle!==false) ) { + fclose($this->OutputHandle); + $this->OutputHandle = false; + } + } + + // ---------------- + // Reading functions + // ---------------- + + function _MoveTo($pos, $relative = SEEK_SET) { + fseek($this->ArchHnd, $pos, $relative); + } + + function _ReadData($len) { + if ($len>0) { + $x = fread($this->ArchHnd, $len); + return $x; + } else { + return ''; + } + } + + // ---------------- + // Take info from binary data + // ---------------- + + function _GetDec($txt, $pos, $len) { + $x = substr($txt, $pos, $len); + $z = 0; + for ($i=0;$i<$len;$i++) { + $asc = ord($x[$i]); + if ($asc>0) $z = $z + $asc*pow(256,$i); + } + return $z; + } + + function _GetHex($txt, $pos, $len) { + $x = substr($txt, $pos, $len); + return 'h:'.bin2hex(strrev($x)); + } + + function _GetBin($txt, $pos, $len) { + $x = substr($txt, $pos, $len); + $z = ''; + for ($i=0;$i<$len;$i++) { + $asc = ord($x[$i]); + if (isset($x[$i])) { + for ($j=0;$j<8;$j++) { + $z .= ($asc & pow(2,$j)) ? '1' : '0'; + } + } else { + $z .= '00000000'; + } + } + return 'b:'.$z; + } + + // ---------------- + // Put info into binary data + // ---------------- + + function _PutDec(&$txt, $val, $pos, $len) { + $x = ''; + for ($i=0;$i<$len;$i++) { + if ($val==0) { + $z = 0; + } else { + $z = intval($val % 256); + if (($val<0) && ($z!=0)) { // ($z!=0) is very important, example: val=-420085702 + // special opration for negative value. If the number id too big, PHP stores it into a signed integer. For example: crc32('coucou') => -256185401 instead of 4038781895. NegVal = BigVal - (MaxVal+1) = BigVal - 256^4 + $val = ($val - $z)/256 -1; + $z = 256 + $z; + } else { + $val = ($val - $z)/256; + } + } + $x .= chr($z); + } + $txt = substr_replace($txt, $x, $pos, $len); + } + + function _MsDos_Date($Timestamp = false) { + // convert a date-time timstamp into the MS-Dos format + $d = ($Timestamp===false) ? getdate() : getdate($Timestamp); + return (($d['year']-1980)*512) + ($d['mon']*32) + $d['mday']; + } + function _MsDos_Time($Timestamp = false) { + // convert a date-time timstamp into the MS-Dos format + $d = ($Timestamp===false) ? getdate() : getdate($Timestamp); + return ($d['hours']*2048) + ($d['minutes']*32) + intval($d['seconds']/2); // seconds are rounded to an even number in order to save 1 bit + } + + function _MsDos_Debug($date, $time) { + // Display the formated date and time. Just for debug purpose. + // date end time are encoded on 16 bits (2 bytes) : date = yyyyyyymmmmddddd , time = hhhhhnnnnnssssss + $y = ($date & 65024)/512 + 1980; + $m = ($date & 480)/32; + $d = ($date & 31); + $h = ($time & 63488)/2048; + $i = ($time & 1984)/32; + $s = ($time & 31) * 2; // seconds have been rounded to an even number in order to save 1 bit + return $y.'-'.str_pad($m,2,'0',STR_PAD_LEFT).'-'.str_pad($d,2,'0',STR_PAD_LEFT).' '.str_pad($h,2,'0',STR_PAD_LEFT).':'.str_pad($i,2,'0',STR_PAD_LEFT).':'.str_pad($s,2,'0',STR_PAD_LEFT); + } + + function _TxtPos($pos) { + // Return the human readable position in both decimal and hexa + return $pos." (h:".dechex($pos).")"; + } + + /** + * Search the record of end of the Central Directory. + * Return the position of the record in the file. + * Return false if the record is not found. The comment cannot exceed 65335 bytes (=FFFF). + * The method is read backwards a block of 256 bytes and search the key in this block. + */ + function _FindCDEnd($cd_info) { + $nbr = 1; + $p = false; + $pos = ftell($this->ArchHnd) - 4 - 256; + while ( ($p===false) && ($nbr<256) ) { + if ($pos<=0) { + $pos = 0; + $nbr = 256; // in order to make this a last check + } + $this->_MoveTo($pos); + $x = $this->_ReadData(256); + $p = strpos($x, $cd_info); + if ($p===false) { + $nbr++; + $pos = $pos - 256 - 256; + } else { + return $pos + $p; + } + } + return false; + } + + function _DataOuputAddedFile($Idx, $PosLoc) { + + $Ref =& $this->AddInfo[$Idx]; + $this->_DataPrepare($Ref); // get data from external file if necessary + + // Other info + $now = time(); + $date = $this->_MsDos_Date($now); + $time = $this->_MsDos_Time($now); + $len_n = strlen($Ref['name']); + $purp = 2048 ; // purpose // +8 to indicates that there is an extended local header + + // Header for file in the data section + $b = 'PK'.chr(03).chr(04).str_repeat(' ',26); // signature + $this->_PutDec($b,20,4,2); //vers = 20 + $this->_PutDec($b,$purp,6,2); // purp + $this->_PutDec($b,$Ref['meth'],8,2); // meth + $this->_PutDec($b,$time,10,2); // time + $this->_PutDec($b,$date,12,2); // date + $this->_PutDec($b,$Ref['crc32'],14,4); // crc32 + $this->_PutDec($b,$Ref['len_c'],18,4); // l_data_c + $this->_PutDec($b,$Ref['len_u'],22,4); // l_data_u + $this->_PutDec($b,$len_n,26,2); // l_name + $this->_PutDec($b,0,28,2); // l_fields + $b .= $Ref['name']; // name + $b .= ''; // fields + + // Output the data + $this->OutputFromString($b.$Ref['data']); + $OutputLen = strlen($b) + $Ref['len_c']; // new position of the cursor + unset($Ref['data']); // save PHP memory + + // Information for file in the Central Directory + $b = 'PK'.chr(01).chr(02).str_repeat(' ',42); // signature + $this->_PutDec($b,20,4,2); // vers_used = 20 + $this->_PutDec($b,20,6,2); // vers_necess = 20 + $this->_PutDec($b,$purp,8,2); // purp + $this->_PutDec($b,$Ref['meth'],10,2); // meth + $this->_PutDec($b,$time,12,2); // time + $this->_PutDec($b,$date,14,2); // date + $this->_PutDec($b,$Ref['crc32'],16,4); // crc32 + $this->_PutDec($b,$Ref['len_c'],20,4); // l_data_c + $this->_PutDec($b,$Ref['len_u'],24,4); // l_data_u + $this->_PutDec($b,$len_n,28,2); // l_name + $this->_PutDec($b,0,30,2); // l_fields + $this->_PutDec($b,0,32,2); // l_comm + $this->_PutDec($b,0,34,2); // disk_num + $this->_PutDec($b,0,36,2); // int_file_att + $this->_PutDec($b,0,38,4); // ext_file_att + $this->_PutDec($b,$PosLoc,42,4); // p_loc + $b .= $Ref['name']; // v_name + $b .= ''; // v_fields + $b .= ''; // v_comm + + $Ref['bin'] = $b; + + return $OutputLen; + + } + + function _DataCreateNewRef($Data, $DataType, $Compress, $Diff, $NameOrIdx) { + + if (is_array($Compress)) { + $result = 2; + $meth = $Compress['meth']; + $len_u = $Compress['len_u']; + $crc32 = $Compress['crc32']; + $Compress = false; + } elseif ($Compress and ($this->Meth8Ok)) { + $result = 1; + $meth = 8; + $len_u = false; // means unknown + $crc32 = false; + } else { + $result = ($Compress) ? -1 : 0; + $meth = 0; + $len_u = false; + $crc32 = false; + $Compress = false; + } + + if ($DataType==TBSZIP_STRING) { + $path = false; + if ($Compress) { + // we compress now in order to save PHP memory + $len_u = strlen($Data); + $crc32 = crc32($Data); + $Data = gzdeflate($Data); + $len_c = strlen($Data); + } else { + $len_c = strlen($Data); + if ($len_u===false) { + $len_u = $len_c; + $crc32 = crc32($Data); + } + } + } else { + $path = $Data; + $Data = false; + if (file_exists($path)) { + $fz = filesize($path); + if ($len_u===false) $len_u = $fz; + $len_c = ($Compress) ? false : $fz; + } else { + return $this->RaiseError("Cannot add the file '".$path."' because it is not found."); + } + } + + // at this step $Data and $crc32 can be false only in case of external file, and $len_c is false only in case of external file to compress + return array('data'=>$Data, 'path'=>$path, 'meth'=>$meth, 'len_u'=>$len_u, 'len_c'=>$len_c, 'crc32'=>$crc32, 'diff'=>$Diff, 'res'=>$result); + + } + + /** + * Returns the real size of data + */ + function _DataPrepare(&$Ref) { + if ($Ref['path']!==false) { + $Ref['data'] = file_get_contents($Ref['path']); + if ($Ref['crc32']===false) $Ref['crc32'] = crc32($Ref['data']); + if ($Ref['len_c']===false) { + // means the data must be compressed + $Ref['data'] = gzdeflate($Ref['data']); + $Ref['len_c'] = strlen($Ref['data']); + } + } + } + + /** + * Return the size of the new archive, or false if it cannot be calculated (because of external file that must be compressed before to be insered) + */ + function _EstimateNewArchSize($Optim=true) { + + if ($this->ArchIsNew) { + $Len = strlen($this->CdInfo['bin']); + } elseif ($this->ArchIsStream) { + $x = fstat($this->ArchHnd); + $Len = $x['size']; + } else { + $Len = filesize($this->ArchFile); + } + + // files to replace or delete + foreach ($this->ReplByPos as $i) { + $Ref =& $this->ReplInfo[$i]; + if ($Ref===false) { + // file to delete + $Info =& $this->CdFileLst[$i]; + if (!isset($this->VisFileLst[$i])) { + if ($Optim) return false; // if $Optimization is set to true, then we d'ont rewind to read information + $this->_MoveTo($Info['p_loc']); + $this->_ReadFile($i, false); + } + $Vis =& $this->VisFileLst[$i]; + $Len += -strlen($Vis['bin']) -strlen($Info['bin']) - $Info['l_data_c']; + if (isset($Vis['desc_bin'])) $Len += -strlen($Vis['desc_bin']); + } elseif ($Ref['len_c']===false) { + return false; // information not yet known + } else { + // file to replace + $Len += $Ref['len_c'] + $Ref['diff']; + } + } + + // files to add + $i_lst = array_keys($this->AddInfo); + foreach ($i_lst as $i) { + $Ref =& $this->AddInfo[$i]; + if ($Ref['len_c']===false) { + return false; // information not yet known + } else { + $Len += $Ref['len_c'] + $Ref['diff']; + } + } + + return $Len; + + } + +} diff --git a/modulebuilder.txt b/modulebuilder.txt new file mode 100755 index 0000000..670a177 --- /dev/null +++ b/modulebuilder.txt @@ -0,0 +1,3 @@ +# DO NOT DELETE THIS FILE MANUALLY +# File to flag module built using official module template. +# When this file is present into a module directory, you can edit it with the module builder tool. \ No newline at end of file diff --git a/prototyp/createodt.php b/prototyp/createodt.php new file mode 100644 index 0000000..85f9e64 --- /dev/null +++ b/prototyp/createodt.php @@ -0,0 +1,89 @@ +open($templateOdt) !== TRUE) { + die("Fehler beim Öffnen von $templateOdt\n"); +} +$zip->extractTo($tmpDir); +$zip->close(); + +// 2. Bild nach Pictures kopieren +if (!is_dir("$tmpDir/Pictures")) { + mkdir("$tmpDir/Pictures"); +} +copy($imageFile, "$tmpDir/Pictures/qrcode.png"); + +// 3. manifest.xml aktualisieren (WICHTIG!) +$manifestFile = "$tmpDir/META-INF/manifest.xml"; +$manifest = file_get_contents($manifestFile); +// Prüfen ob Eintrag schon existiert +if (strpos($manifest, 'Pictures/qrcode.png') === false) { + $newEntry = ''; + $manifest = str_replace('', $newEntry . "\n", $manifest); + file_put_contents($manifestFile, $manifest); +} + +// 4. content.xml laden +$contentFile = "$tmpDir/content.xml"; +$content = file_get_contents($contentFile); + +// 5. Bild-XML (ODF-konform) +$imageXml = '' . +'' . +''; + +// {qrcode} ersetzen +$content = str_replace('{qrcode}', $imageXml, $content); +file_put_contents($contentFile, $content); + +// 6. Neue ODT bauen +$zip = new ZipArchive(); +if ($zip->open($outputOdt, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE) { + die("Fehler beim Erstellen von $outputOdt\n"); +} + +// 6a. mimetype MUSS zuerst & unkomprimiert +$zip->addFile("$tmpDir/mimetype", "mimetype"); +$zip->setCompressionName("mimetype", ZipArchive::CM_STORE); + +// 6b. Restliche Dateien rekursiv hinzufügen +$files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($tmpDir, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::LEAVES_ONLY +); + +foreach ($files as $file) { + if (!$file->isFile()) continue; + $filePath = $file->getRealPath(); + $relativePath = substr($filePath, strlen($tmpDir) + 1); + + // mimetype überspringen (schon hinzugefügt) + if ($relativePath === 'mimetype') continue; + + // Backslashes zu Forward-Slashes (für Windows-Kompatibilität) + $relativePath = str_replace('\\', '/', $relativePath); + + $zip->addFile($filePath, $relativePath); +} + +$zip->close(); + +// 7. Cleanup +function deleteDir($dir) { + if (!is_dir($dir)) return; + foreach (scandir($dir) as $item) { + if ($item == '.' || $item == '..') continue; + $path = "$dir/$item"; + is_dir($path) ? deleteDir($path) : unlink($path); + } + rmdir($dir); +} +deleteDir($tmpDir); + +echo "Fertig: $outputOdt\n"; diff --git a/prototyp/index.php b/prototyp/index.php new file mode 100755 index 0000000..4f79513 --- /dev/null +++ b/prototyp/index.php @@ -0,0 +1,3 @@ + diff --git a/prototyp/output.odt b/prototyp/output.odt new file mode 100644 index 0000000..b9ba8cc Binary files /dev/null and b/prototyp/output.odt differ diff --git a/prototyp/output.pdf b/prototyp/output.pdf new file mode 100644 index 0000000..ccaa093 Binary files /dev/null and b/prototyp/output.pdf differ diff --git a/prototyp/output/META-INF/manifest.xml b/prototyp/output/META-INF/manifest.xml new file mode 100644 index 0000000..b76afd1 --- /dev/null +++ b/prototyp/output/META-INF/manifest.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/prototyp/output/Pictures/10000001000002CC000002BB0E6EE63D.png b/prototyp/output/Pictures/10000001000002CC000002BB0E6EE63D.png new file mode 100644 index 0000000..cab7f3d Binary files /dev/null and b/prototyp/output/Pictures/10000001000002CC000002BB0E6EE63D.png differ diff --git a/prototyp/output/Pictures/1000000100000ED50000085C4741BD09.png b/prototyp/output/Pictures/1000000100000ED50000085C4741BD09.png new file mode 100644 index 0000000..ef3908f Binary files /dev/null and b/prototyp/output/Pictures/1000000100000ED50000085C4741BD09.png differ diff --git a/prototyp/output/Pictures/1002499C00005A970000587B4DBA0F1F.svg b/prototyp/output/Pictures/1002499C00005A970000587B4DBA0F1F.svg new file mode 100644 index 0000000..0f19c46 --- /dev/null +++ b/prototyp/output/Pictures/1002499C00005A970000587B4DBA0F1F.svg @@ -0,0 +1,293 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/prototyp/output/Pictures/qrcode.png b/prototyp/output/Pictures/qrcode.png new file mode 100644 index 0000000..a3f8a47 Binary files /dev/null and b/prototyp/output/Pictures/qrcode.png differ diff --git a/prototyp/output/Thumbnails/thumbnail.png b/prototyp/output/Thumbnails/thumbnail.png new file mode 100644 index 0000000..95726ed Binary files /dev/null and b/prototyp/output/Thumbnails/thumbnail.png differ diff --git a/prototyp/output/content.xml b/prototyp/output/content.xml new file mode 100644 index 0000000..86f9f7a --- /dev/null +++ b/prototyp/output/content.xml @@ -0,0 +1,2 @@ + +{mycompany_name} – {mycompany_address} –{mycompany_zip} – {mycompany_town}[!-- IF {billing_contact_name} --] {billing_contact_name} {billing_contact_firstname} [!-- ELSE {billing_contact_name} --] {company_name} [!-- ENDIF {billing_contact_name} --][!-- IF {billing_contact_address} --] {billing_contact_address} [!-- ELSE {billing_contact_address} --] {company_address} [!-- ENDIF {billing_contact_address} --][!-- IF {billing_contact_zip} --] {billing_contact_zip} {billing_contact_town}[!-- ELSE {billing_contact_zip} --] {company_zip} {company_town}[!-- ENDIF {billing_contact_zip} --]RechnungsdetailsE-Mail:{mycompany_email}Telefon:{mycompany_phone}Rechnungsdatum:{object_date}Kunden-Nr.:{company_customercode}Auftrags-Nr.:[!-- IF {object_options_auftragsnummer} --] {object_options_auftragsnummer} [!-- ELSE {object_options_auftragsnummer} --] Keine Auftragsnummer [!-- ENDIF {object_options_auftragsnummer} --]Angebots-Nr.:[!-- IF {object_options_angebotsnummer} --] {object_options_angebotsnummer} [!-- ELSE {object_options_angebotsnummer} --] Keine Angebotsnummer [!-- ENDIF {object_options_angebotsnummer} --]Zahlbar: {object_payment_term}[!-- IF {object_options_hinweis} --] Hinweis: {object_options_hinweis} [!-- ENDIF {object_options_hinweis} --]Rechnungs-Nr.: {object_ref}[!-- IF {delivery_contact_name} --] Objekt: ({delivery_contact_name}) - {delivery_contact_address}, {delivery_contact_zip} {delivery_contact_town} [!-- ENDIF {delivery_contact_name} --]Pos.MengeBeschreibungEinzelpreisGesamt[!-- BEGIN row.lines --]{line_pos}{line_qty} {line_product_barcode}{line_product_label}{line_desc}{line_up_locale} {line_price_ht_locale}[!-- END row.lines --]Summe Artikel (netto){object_total_ht_locale} Mwst. 19,0%:{object_total_vat_locale} Gesamtsumme{object_total_ttc_locale} \ No newline at end of file diff --git a/prototyp/output/manifest.rdf b/prototyp/output/manifest.rdf new file mode 100644 index 0000000..927e206 --- /dev/null +++ b/prototyp/output/manifest.rdf @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/prototyp/output/meta.xml b/prototyp/output/meta.xml new file mode 100644 index 0000000..9dc4a9b --- /dev/null +++ b/prototyp/output/meta.xml @@ -0,0 +1,2 @@ + +2025-07-18T09:14:24.1204983222026-01-10T20:10:31.182585456PT14H56M45S190LibreOffice/25.2.7.2$Linux_X86_64 LibreOffice_project/520$Build-2 \ No newline at end of file diff --git a/prototyp/output/mimetype b/prototyp/output/mimetype new file mode 100644 index 0000000..2e95b81 --- /dev/null +++ b/prototyp/output/mimetype @@ -0,0 +1 @@ +application/vnd.oasis.opendocument.text \ No newline at end of file diff --git a/prototyp/output/settings.xml b/prototyp/output/settings.xml new file mode 100644 index 0000000..8aaf67d --- /dev/null +++ b/prototyp/output/settings.xml @@ -0,0 +1,2 @@ + +003672217946truefalseview223964616300367211794401false140falsefalsetruetruefalsefalsefalsetruetruefalse0falsetruefalsefalsefalsetruefalsefalsefalse0truefalsefalsefalsefalsefalsetruefalsefalsetruefalsetruetruefalsefalsefalsefalsefalsefalsefalsetruefalsetruefalsefalsefalsefalsefalsefalsehigh-resolutionfalse1968361falsetruefalsetruefalsefalsefalsetruetruetruefalse0truefalsefalsetruetruetruefalsetruefalsefalsefalsefalsefalsetruefalsefalse0falsetruefalsefalsetruetruefalsefalsetrue1falsefalsefalsefalsefalsefalsefalsetruefalsefalsefalsetruetruetruefalsetruefalsetrue15579466truefalsefalse \ No newline at end of file diff --git a/prototyp/output/styles.xml b/prototyp/output/styles.xml new file mode 100644 index 0000000..4ff5e15 --- /dev/null +++ b/prototyp/output/styles.xml @@ -0,0 +1,2 @@ + + - RechnungGemäß § 14b Abs. 1 UStG ist diese Rechnung mindestens zwei Jahre aufzubewahren.Die gelieferte Ware bleibt bis zur vollständigen Bezahlung Eigentum der Firma Alles Watt läuft.Umsatzsteueridentifikationsnummer: {mycompany_vatnumber}Kontoinhaber: Eduard Wisch - Bank: VR Bank Westküste - IBAN: DE70 2176 2550 0013 4381 47BIC: GENODEF1HUM - Verwendungszweck: {object_ref}Seite 2/ 2 \ No newline at end of file diff --git a/prototyp/qr.png b/prototyp/qr.png new file mode 100644 index 0000000..a3f8a47 Binary files /dev/null and b/prototyp/qr.png differ diff --git a/prototyp/test.odt b/prototyp/test.odt new file mode 100755 index 0000000..e9ff555 Binary files /dev/null and b/prototyp/test.odt differ diff --git a/sql/.kdev4/sql.kdev4 b/sql/.kdev4/sql.kdev4 new file mode 100755 index 0000000..e06adee --- /dev/null +++ b/sql/.kdev4/sql.kdev4 @@ -0,0 +1,2 @@ +[Buildset] +BuildItems=@Variant(\x00\x00\x00\t\x00\x00\x00\x00\x01\x00\x00\x00\x0b\x00\x00\x00\x00\x01\x00\x00\x00\x06\x00s\x00q\x00l) diff --git a/sql/dolibarr_allversions.sql b/sql/dolibarr_allversions.sql new file mode 100755 index 0000000..5026bb4 --- /dev/null +++ b/sql/dolibarr_allversions.sql @@ -0,0 +1,3 @@ +-- +-- Script run when an upgrade of Dolibarr is done. Whatever is the Dolibarr version. +-- diff --git a/sql/sql.kdev4 b/sql/sql.kdev4 new file mode 100755 index 0000000..f7fcb7f --- /dev/null +++ b/sql/sql.kdev4 @@ -0,0 +1,4 @@ +[Project] +CreatedFrom= +Manager=KDevCustomBuildSystem +Name=sql diff --git a/sql/update_1.5.0.sql b/sql/update_1.5.0.sql new file mode 100644 index 0000000..23f63a2 --- /dev/null +++ b/sql/update_1.5.0.sql @@ -0,0 +1,74 @@ +-- Copyright (C) 2025 Eduard Wisch +-- +-- 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 https://www.gnu.org/licenses/. + +-- +-- Update-Script für EPCQR-Modul Version 1.5.0 +-- Fügt Extrafeld für lokalen QR-Code-Pfad hinzu +-- + +-- Extrafeld für QR-Code Dateipfad (für ODT-Integration) +-- Dieses Feld enthält den lokalen Dateipfad zum generierten QR-Code-Bild + +INSERT INTO llx_extrafields ( + name, + label, + type, + size, + elementtype, + fieldunique, + fieldrequired, + pos, + alwayseditable, + param, + enabled, + perms, + list, + totalizable, + printable, + langs, + help, + computed, + entity +) VALUES ( + 'qrcodepath', + 'QR-Code Bildpfad (lokal)', + 'varchar', + '255', + 'facture', + 0, + 0, + 900, + 0, + '', + '1', + '', + '0', + 0, + 0, + NULL, + 'Lokaler Pfad zur QR-Code-Bilddatei für ODT-Integration', + '', + 0 +) +ON DUPLICATE KEY UPDATE + label = 'QR-Code Bildpfad (lokal)', + type = 'varchar', + size = '255'; + +-- Hinweis: Die bestehenden Extrafelder 'qrcode' und 'qrcodepfad' bleiben +-- aus Kompatibilitätsgründen bestehen: +-- - qrcode: HTML-Version mit -Tag +-- - qrcodepfad: URL zum QR-Code (alt: externer Service, neu: viewimage.php) +-- - qrcodepath: Lokaler Dateipfad (NEU, für ODT-Verarbeitung) diff --git a/test_qrcode.php b/test_qrcode.php new file mode 100644 index 0000000..5455324 --- /dev/null +++ b/test_qrcode.php @@ -0,0 +1,285 @@ +EPCQR Modul - Test & Debug'; + +// 1. Modul-Status prüfen +print '

1. Modul-Status

'; +print ''; +print ''; + +// 2. Modul-Konfiguration prüfen +print ''; + +print ''; + +print '
Modul aktiviert'; +print isModEnabled('epcqr') ? '✓ Ja' : '✗ Nein'; +print '
Substitutionen aktiviert'; +$module_parts = $conf->modules_parts; +$subst_active = isset($module_parts['substitutions']) && in_array('epcqr', $module_parts['substitutions']); +print $subst_active ? '✓ Ja' : '✗ Nein (in modEpcqr.class.php aktivieren!)'; +print '
Hooks aktiviert'; +$hooks_active = isset($module_parts['hooks']) && isset($module_parts['hooks']['epcqr']); +print $hooks_active ? '✓ Ja' : '✗ Nein'; +if ($hooks_active) { + print '
Hooks: '.implode(', ', $module_parts['hooks']['epcqr']); +} +print '
'; + +// 3. Dateien prüfen +print '

2. Dateien vorhanden

'; +print ''; + +$files = array( + 'QR-Code Generator' => __DIR__.'/lib/qrcode.class.php', + 'Substitutionen' => __DIR__.'/core/substitutions/functions_epcqr.lib.php', + 'Hook-Klasse' => __DIR__.'/class/actions_epcqr.class.php', + 'Hilfsfunktionen' => __DIR__.'/lib/epcqr.lib.php', +); + +foreach ($files as $name => $file) { + print ''; +} + +print '
'.$name.''; + if (file_exists($file)) { + print '✓ Vorhanden ('.$file.')'; + } else { + print '✗ Fehlt! ('.$file.')'; + } + print '
'; + +// 4. Extrafelder prüfen +print '

3. Extrafelder

'; +print ''; + +require_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php'; +$extrafields = new ExtraFields($db); +$extrafields->fetch_name_optionals_label('facture'); + +$required_fields = array('qrcode', 'qrcodepfad', 'qrcodepath'); +foreach ($required_fields as $field) { + print ''; +} + +print '
'.$field.''; + if (isset($extrafields->attributes['facture']['label'][$field])) { + print '✓ Vorhanden'; + print ' - Typ: '.$extrafields->attributes['facture']['type'][$field]; + } else { + print '✗ Fehlt! (SQL-Update ausführen!)'; + } + print '
'; + +// 5. Verzeichnisse prüfen +print '

4. Verzeichnisse

'; +print ''; + +$qrCodeDir = $conf->epcqr->dir_output.'/qrcodes'; +print ''; + +print '
QR-Code Cache'; +if (is_dir($qrCodeDir)) { + print '✓ Vorhanden ('.$qrCodeDir.')'; + if (is_writable($qrCodeDir)) { + print ' - Beschreibbar'; + } else { + print ' - Nicht beschreibbar!'; + } +} else { + print '⚠ Fehlt (wird automatisch erstellt)'; +} +print '
'; + +// 6. Test: QR-Code generieren +print '

5. Test: QR-Code generieren

'; + +require_once __DIR__.'/lib/qrcode.class.php'; + +try { + $qrGen = new QRCodeGenerator($db); + $testPath = $qrGen->generateEPCQRCode( + 'Test Firma', + 'DE89370400440532013000', + 'COBADEFFXXX', + 99.99, + 'TEST-001' + ); + + print '

✓ QR-Code erfolgreich generiert!

'; + print '

Pfad: '.$testPath.'

'; + + if (file_exists($testPath)) { + print '

'; + } +} catch (Exception $e) { + print '

✗ Fehler: '.$e->getMessage().'

'; +} + +// 7. Letzte Rechnungen mit QR-Codes prüfen +print '

6. Letzte 5 Rechnungen mit QR-Codes

'; + +$sql = "SELECT f.rowid, f.ref, f.total_ttc, f.datef"; +$sql .= " FROM ".MAIN_DB_PREFIX."facture as f"; +$sql .= " WHERE f.entity = ".((int) $conf->entity); +$sql .= " ORDER BY f.datef DESC"; +$sql .= " LIMIT 5"; + +$resql = $db->query($sql); +if ($resql) { + $num = $db->num_rows($resql); + + if ($num > 0) { + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + + while ($obj = $db->fetch_object($resql)) { + $invoice = new Facture($db); + $invoice->fetch($obj->rowid); + $invoice->fetch_optionals(); + + print ''; + print ''; + print ''; + + $qrPath = isset($invoice->array_options['options_qrcodepath']) ? $invoice->array_options['options_qrcodepath'] : ''; + print ''; + } else { + print 'Nicht gesetzt'; + print ''; + } + print ''; + } + + print '
RechnungBetragQR-Code PathDatei existiert?
'.$invoice->getNomUrl(1).''.price($obj->total_ttc).''; + if (!empty($qrPath)) { + print $qrPath; + print ''; + if (file_exists($qrPath)) { + print '✓ Ja'; + } else { + print '✗ Nein'; + } + print '-
'; + } else { + print '

Keine Rechnungen gefunden.

'; + } +} else { + print '

SQL-Fehler: '.$db->lasterror().'

'; +} + +// 8. Logs prüfen +print '

7. Letzte Log-Einträge (EPCQR)

'; + +$logFile = DOL_DATA_ROOT.'/dolibarr.log'; +if (file_exists($logFile)) { + $lines = file($logFile); + $epcqrLogs = array(); + + foreach (array_reverse($lines) as $line) { + if (stripos($line, 'EPCQR') !== false || stripos($line, 'epcqr') !== false) { + $epcqrLogs[] = $line; + if (count($epcqrLogs) >= 10) break; + } + } + + if (!empty($epcqrLogs)) { + print '
';
+        foreach (array_reverse($epcqrLogs) as $log) {
+            print htmlspecialchars($log);
+        }
+        print '
'; + } else { + print '

Keine EPCQR-Log-Einträge gefunden.

'; + } +} else { + print '

Log-Datei nicht gefunden: '.$logFile.'

'; +} + +// 9. Anleitung +print '

8. Nächste Schritte

'; +print '
'; +print '
    '; +print '
  1. Modul neu aktivieren: Wenn Substitutionen oder Hooks rot sind → Modul deaktivieren und wieder aktivieren
  2. '; +print '
  3. Rechnung validieren: Erstellen Sie eine Test-Rechnung und validieren Sie diese
  4. '; +print '
  5. ODT generieren: Template mit {qrcode} erstellen und Dokument generieren
  6. '; +print '
  7. Logs prüfen: Schauen Sie in documents/dolibarr.log nach EPCQR-Einträgen
  8. '; +print '
'; +print '
'; + +print '

9. Schnelltest: ODT-Bildverarbeitung

'; +print '
'; +print ''; +print '

Test-Rechnung ID:

'; +print ''; +print '
'; + +if (GETPOST('action') == 'test_odt' && GETPOST('invoice_id')) { + $invoice_id = GETPOST('invoice_id', 'int'); + + print '

Test-Ergebnis:

'; + + $invoice = new Facture($db); + if ($invoice->fetch($invoice_id) > 0) { + $invoice->fetch_optionals(); + + print '

Rechnung: '.$invoice->ref.'

'; + + // QR-Code Path prüfen + $qrPath = isset($invoice->array_options['options_qrcodepath']) ? $invoice->array_options['options_qrcodepath'] : ''; + print '

QR-Code Path: '.($qrPath ? $qrPath : 'Nicht gesetzt!').'

'; + + if (!empty($qrPath) && file_exists($qrPath)) { + print '

✓ QR-Code-Datei existiert

'; + + // Test: Bilddaten sammeln + require_once __DIR__.'/lib/epcqr.lib.php'; + require_once __DIR__.'/core/substitutions/functions_epcqr.lib.php'; + + $imageData = array(); + if (!empty($qrPath) && file_exists($qrPath)) { + $imageData['qrcode'] = $qrPath; + } + + print '

Bilddaten gesammelt: '.count($imageData).' Bilder

'; + + if (!empty($imageData)) { + print '
';
+                print_r($imageData);
+                print '
'; + } + } else { + print '

✗ QR-Code-Datei nicht gefunden

'; + print '

Lösung: Rechnung neu validieren (Draft → Validate)

'; + } + } else { + print '

Rechnung nicht gefunden!

'; + } +} + +// Footer +llxFooter(); +$db->close();